internal/cmd/scan.go (174 lines of code) (raw):
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/JetBrains/qodana-cli/internal/cloud"
"github.com/JetBrains/qodana-cli/internal/core/corescan"
"github.com/JetBrains/qodana-cli/internal/core/startup"
"github.com/JetBrains/qodana-cli/internal/platform"
"github.com/JetBrains/qodana-cli/internal/platform/cmd"
"github.com/JetBrains/qodana-cli/internal/platform/commoncontext"
"github.com/JetBrains/qodana-cli/internal/platform/effectiveconfig"
"github.com/JetBrains/qodana-cli/internal/platform/msg"
"github.com/JetBrains/qodana-cli/internal/platform/qdenv"
"github.com/JetBrains/qodana-cli/internal/platform/qdyaml"
"github.com/JetBrains/qodana-cli/internal/platform/utils"
log "github.com/sirupsen/logrus"
"github.com/JetBrains/qodana-cli/internal/core"
"github.com/spf13/cobra"
)
// newScanCommand returns a new instance of the scan command.
func newScanCommand() *cobra.Command {
cliOptions := &platformcmd.CliOptions{}
c := &cobra.Command{
Use: "scan",
Short: "Scan project with Qodana",
Long: `Scan a project with Qodana. It runs one of Qodana's Docker images (https://www.jetbrains.com/help/qodana/docker-images.html) and reports the results.
Note that most options can be configured via qodana.yaml (https://www.jetbrains.com/help/qodana/qodana-yaml.html) file.
But you can always override qodana.yaml options with the following command-line options.
`,
Run: func(cmd *cobra.Command, args []string) {
qdenv.InitializeQodanaGlobalEnv(cliOptions)
ctx := cmd.Context()
commonCtx := commoncontext.Compute(
cliOptions.Linter,
cliOptions.Ide,
cliOptions.Image,
cliOptions.WithinDocker,
cliOptions.CacheDir,
cliOptions.ResultsDir,
cliOptions.ReportDir,
qdenv.GetQodanaGlobalEnv(qdenv.QodanaToken),
cliOptions.ClearCache,
cliOptions.ProjectDir,
cliOptions.RepositoryRoot,
cliOptions.ConfigName,
)
oldReportUrl := cloud.GetReportUrl(commonCtx.ResultsDir)
checkProjectDir(commonCtx.ProjectDir)
preparedHost := startup.PrepareHost(commonCtx)
effectiveConfigFiles := effectiveconfig.Files{}
qodanaYamlConfig := corescan.QodanaYamlConfig{}
if !commonCtx.Analyzer.IsContainer() {
var err error
localQodanaYamlFullPath := qdyaml.GetLocalNotEffectiveQodanaYamlFullPath(
commonCtx.ProjectDir,
cliOptions.ConfigName,
)
effectiveConfigDir, cleanup, err := utils.CreateTempDir("qd-effective-config")
if err != nil {
log.Fatalf("Failed to create effective config directory: %v", err)
}
defer cleanup()
effectiveConfigFiles, err = effectiveconfig.CreateEffectiveConfigFiles(
localQodanaYamlFullPath,
cliOptions.GlobalConfigurationsDir,
cliOptions.GlobalConfigurationId,
preparedHost.Prod.JbrJava(),
effectiveConfigDir,
commonCtx.LogDir(),
)
if err != nil {
log.Fatalf("Failed to load Qodana configuration %s", err)
}
if effectiveConfigFiles.EffectiveQodanaYamlPath != "" {
yaml := qdyaml.LoadQodanaYamlByFullPath(effectiveConfigFiles.EffectiveQodanaYamlPath)
qodanaYamlConfig = corescan.YamlConfig(yaml)
}
}
scanContext := corescan.CreateContext(
*cliOptions,
commonCtx,
preparedHost,
qodanaYamlConfig,
effectiveConfigFiles.ConfigDir,
)
exitCode := core.RunAnalysis(ctx, scanContext)
if qdenv.IsContainer() {
err := platform.ChangeResultsPermissionsRecursively(scanContext.ResultsDir())
if err != nil {
msg.ErrorMessage("Unable to change permissions in %s: %s", scanContext.ResultsDir(), err)
}
}
checkExitCode(exitCode, scanContext)
newReportUrl := cloud.GetReportUrl(scanContext.ResultsDir())
platform.ProcessSarif(
filepath.Join(scanContext.ResultsDir(), commoncontext.QodanaSarifName),
scanContext.AnalysisId(),
newReportUrl,
scanContext.PrintProblems(),
scanContext.GenerateCodeClimateReport(),
scanContext.SendBitBucketInsights(),
)
showReport := scanContext.ShowReport()
if msg.IsInteractive() {
showReport = msg.AskUserConfirm("Do you want to open the latest report")
}
if newReportUrl != oldReportUrl && newReportUrl != "" && !qdenv.IsContainer() {
msg.SuccessMessage("Report is successfully uploaded to %s", newReportUrl)
}
if showReport {
commoncontext.ShowReport(scanContext.ResultsDir(), scanContext.ReportDir(), scanContext.Port())
} else if !qdenv.IsContainer() && msg.IsInteractive() {
msg.WarningMessage(
"To view the Qodana report later, run %s in the current directory or add %s flag to %s",
msg.PrimaryBold("qodana show"),
msg.PrimaryBold("--show-report"),
msg.PrimaryBold("qodana scan"),
)
}
if exitCode == utils.QodanaFailThresholdExitCode {
msg.EmptyMessage()
msg.ErrorMessage("The number of problems exceeds the fail threshold")
os.Exit(exitCode)
}
},
}
err := platformcmd.ComputeFlags(c, cliOptions)
if err != nil {
return nil
}
return c
}
func checkProjectDir(projectDir string) {
if msg.IsInteractive() && core.IsHomeDirectory(projectDir) {
msg.WarningMessage(
fmt.Sprintf("Project directory (%s) is the $HOME directory", projectDir),
)
if !msg.AskUserConfirm(msg.DefaultPromptText) {
os.Exit(0)
}
}
if !utils.CheckDirFiles(projectDir) {
msg.ErrorMessage("No files to check with Qodana found in %s", projectDir)
os.Exit(1)
}
}
func checkExitCode(exitCode int, c corescan.Context) {
if exitCode == utils.QodanaEapLicenseExpiredExitCode && msg.IsInteractive() {
msg.EmptyMessage()
msg.ErrorMessage(
"Your license expired: update your license or token. If you are using EAP, make sure you are using the latest CLI version and update to the latest linter by running %s ",
msg.PrimaryBold("qodana init"),
)
os.Exit(exitCode)
} else if exitCode == utils.QodanaTimeoutExitCodePlaceholder {
msg.ErrorMessage("Qodana analysis reached timeout %s", c.GetAnalysisTimeout())
os.Exit(c.AnalysisTimeoutExitCode())
} else if exitCode == utils.QodanaEmptyChangesetExitCodePlaceholder {
msg.ErrorMessage("Nothing to analyse. Exiting with %s", utils.QodanaSuccessExitCode)
os.Exit(utils.QodanaSuccessExitCode)
} else if exitCode != utils.QodanaSuccessExitCode && exitCode != utils.QodanaFailThresholdExitCode {
msg.ErrorMessage("Qodana exited with code %d", exitCode)
msg.WarningMessage("Check ./logs/ in the results directory for more information")
if exitCode == utils.QodanaOutOfMemoryExitCode {
msg.ErrorMessage("Qodana was terminated after running out of memory.")
} else if msg.AskUserConfirm(fmt.Sprintf("Do you want to open %s", c.ResultsDir())) {
err := core.OpenDir(c.ResultsDir())
if err != nil {
log.Fatalf("Error while opening directory: %s", err)
}
}
os.Exit(exitCode)
}
}