internal/core/corescan/context.go (363 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 corescan import ( "fmt" "math" "os" "path/filepath" "strings" "time" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/product" "github.com/JetBrains/qodana-cli/internal/platform/qdyaml" ) const ( RunScenarioDefault = "default" RunScenarioFullHistory = "full-history" RunScenarioLocalChanges = "local-changes" RunScenarioScoped = "scope" RunScenarioReversedScoped = "reversed-scope" ) type RunScenario = string // Context // // If one has the instance of Context, then it means that it was initialized, and it is in valid state // all mutations should be defined in context_changes.go with names clearly demonstrating the usecase and business logic. // example: scoped script launches two stages of default analysis // // # In the future, you can consider making it an interface // // Yes, there is a lot of boilerplate, and it's ok, it's much better than having one object written and initialized across your program // if I have an object it means it must be in valid state, otherwise it's impossible to reason about the code // // Q: - Why do we prohibit mutations in Context if we pass it by value already? // A: - Because we want to prohibit any unexpected changes to Context at all, imagine at some place the projectDir will // // suddenly change, and pass it further? It's not clear why it was changed? Was it actually initialized at this place? // but it must be initialized already, so we limit all changes, and keep them in context_changes.go // // !!!KEEP IT IMMUTABLE!!! type Context struct { analyser product.Analyzer id string ideDir string effectiveConfigurationDir string globalConfigurationsDir string globalConfigurationId string customLocalQodanaYamlPath string qodanaYamlConfig QodanaYamlConfig prod product.Product qodanaUploadToken string projectDir string repositoryRoot string resultsDir string configDir string logDir string qodanaSystemDir string cacheDir string reportDir string coverageDir string onlyDirectory string _env []string disableSanity bool profileName string profilePath string runPromo string baseline string baselineIncludeAbsent bool saveReport bool showReport bool port int _property []string script string failThreshold string commit string diffStart string diffEnd string forceLocalChangesScript bool reversePrAnalysis bool reducedScopePath string analysisId string _volumes []string user string printProblems bool generateCodeClimateReport bool sendBitBucketInsights bool skipPull bool fullHistory bool applyFixes bool cleanup bool fixesStrategy string noStatistics bool cdnetSolution string cdnetProject string cdnetConfiguration string cdnetPlatform string cdnetNoBuild bool clangCompileCommands string clangArgs string analysisTimeoutMs int analysisTimeoutExitCode int jvmDebugPort int } // QodanaYamlConfig fields from qodana.yaml used in CLI for core linters (also `linter` and `ide`) type QodanaYamlConfig struct { Bootstrap string Plugins []qdyaml.Plugin Properties map[string]string DotNet qdyaml.DotNet } func YamlConfig(yaml qdyaml.QodanaYaml) QodanaYamlConfig { return QodanaYamlConfig{ Bootstrap: yaml.Bootstrap, Plugins: yaml.Plugins, Properties: yaml.Properties, DotNet: yaml.DotNet, } } func (c Context) Analyser() product.Analyzer { return c.analyser } func (c Context) Id() string { return c.id } func (c Context) IdeDir() string { return c.ideDir } func (c Context) EffectiveConfigurationDir() string { return c.effectiveConfigurationDir } func (c Context) GlobalConfigurationsDir() string { return c.globalConfigurationsDir } func (c Context) GlobalConfigurationId() string { return c.globalConfigurationId } func (c Context) CustomLocalQodanaYamlPath() string { return c.customLocalQodanaYamlPath } func (c Context) QodanaYamlConfig() QodanaYamlConfig { return c.qodanaYamlConfig } func (c Context) Prod() product.Product { return c.prod } func (c Context) QodanaUploadToken() string { return c.qodanaUploadToken } func (c Context) ProjectDir() string { return c.projectDir } func (c Context) RepositoryRoot() string { return c.repositoryRoot } func (c Context) ResultsDir() string { return c.resultsDir } func (c Context) ConfigDir() string { return c.configDir } func (c Context) LogDir() string { return c.logDir } func (c Context) QodanaSystemDir() string { return c.qodanaSystemDir } func (c Context) CacheDir() string { return c.cacheDir } func (c Context) ReportDir() string { return c.reportDir } func (c Context) CoverageDir() string { return c.coverageDir } func (c Context) OnlyDirectory() string { return c.onlyDirectory } func (c Context) DisableSanity() bool { return c.disableSanity } func (c Context) ProfileName() string { return c.profileName } func (c Context) ProfilePath() string { return c.profilePath } func (c Context) RunPromo() string { return c.runPromo } func (c Context) Baseline() string { return c.baseline } func (c Context) BaselineIncludeAbsent() bool { return c.baselineIncludeAbsent } func (c Context) SaveReport() bool { return c.saveReport } func (c Context) ShowReport() bool { return c.showReport } func (c Context) Port() int { return c.port } func (c Context) Script() string { return c.script } func (c Context) FailThreshold() string { return c.failThreshold } func (c Context) Commit() string { return c.commit } func (c Context) DiffStart() string { return c.diffStart } func (c Context) DiffEnd() string { return c.diffEnd } func (c Context) ForceLocalChangesScript() bool { return c.forceLocalChangesScript } func (c Context) ReducedScopePath() string { return c.reducedScopePath } func (c Context) ReversePrAnalysis() bool { return c.reversePrAnalysis } func (c Context) AnalysisId() string { return c.analysisId } func (c Context) User() string { return c.user } func (c Context) PrintProblems() bool { return c.printProblems } func (c Context) GenerateCodeClimateReport() bool { return c.generateCodeClimateReport } func (c Context) SendBitBucketInsights() bool { return c.sendBitBucketInsights } func (c Context) SkipPull() bool { return c.skipPull } func (c Context) FullHistory() bool { return c.fullHistory } func (c Context) ApplyFixes() bool { return c.applyFixes } func (c Context) Cleanup() bool { return c.cleanup } func (c Context) FixesStrategy() string { return c.fixesStrategy } func (c Context) NoStatistics() bool { return c.noStatistics } func (c Context) CdnetSolution() string { return c.cdnetSolution } func (c Context) CdnetProject() string { return c.cdnetProject } func (c Context) CdnetConfiguration() string { return c.cdnetConfiguration } func (c Context) CdnetPlatform() string { return c.cdnetPlatform } func (c Context) CdnetNoBuild() bool { return c.cdnetNoBuild } func (c Context) ClangCompileCommands() string { return c.clangCompileCommands } func (c Context) ClangArgs() string { return c.clangArgs } func (c Context) AnalysisTimeoutMs() int { return c.analysisTimeoutMs } func (c Context) AnalysisTimeoutExitCode() int { return c.analysisTimeoutExitCode } func (c Context) JvmDebugPort() int { return c.jvmDebugPort } func (c Context) Env() []string { return arrayCopy(c._env) } func (c Context) Property() []string { return arrayCopy(c._property) } func (c Context) Volumes() []string { return arrayCopy(c._volumes) } type ContextBuilder struct { Analyser product.Analyzer Id string IdeDir string EffectiveConfigurationDir string Prod product.Product QodanaUploadToken string ProjectDir string RepositoryRoot string ResultsDir string ConfigDir string LogDir string QodanaSystemDir string CacheDir string ReportDir string CoverageDir string OnlyDirectory string Env []string DisableSanity bool ProfileName string ProfilePath string RunPromo string Baseline string BaselineIncludeAbsent bool SaveReport bool ShowReport bool Port int Property []string Script string FailThreshold string Commit string DiffStart string DiffEnd string ForceLocalChangesScript bool ReversePrAnalysis bool AnalysisId string Volumes []string User string PrintProblems bool GenerateCodeClimateReport bool SendBitBucketInsights bool SkipPull bool FullHistory bool ApplyFixes bool Cleanup bool FixesStrategy string NoStatistics bool CdnetSolution string CdnetProject string CdnetConfiguration string CdnetPlatform string CdnetNoBuild bool ClangCompileCommands string ClangArgs string AnalysisTimeoutMs int AnalysisTimeoutExitCode int JvmDebugPort int GlobalConfigurationsDir string GlobalConfigurationId string CustomLocalQodanaYamlPath string QodanaYamlConfig QodanaYamlConfig } func (b ContextBuilder) Build() Context { return Context{ analyser: b.Analyser, id: b.Id, ideDir: b.IdeDir, effectiveConfigurationDir: b.EffectiveConfigurationDir, prod: b.Prod, qodanaUploadToken: b.QodanaUploadToken, projectDir: b.ProjectDir, repositoryRoot: b.RepositoryRoot, resultsDir: b.ResultsDir, configDir: b.ConfigDir, logDir: b.LogDir, qodanaSystemDir: b.QodanaSystemDir, cacheDir: b.CacheDir, reportDir: b.ReportDir, coverageDir: b.CoverageDir, onlyDirectory: b.OnlyDirectory, _env: b.Env, disableSanity: b.DisableSanity, profileName: b.ProfileName, profilePath: b.ProfilePath, runPromo: b.RunPromo, baseline: b.Baseline, baselineIncludeAbsent: b.BaselineIncludeAbsent, saveReport: b.SaveReport, showReport: b.ShowReport, port: b.Port, _property: b.Property, script: b.Script, failThreshold: b.FailThreshold, commit: b.Commit, diffStart: b.DiffStart, diffEnd: b.DiffEnd, forceLocalChangesScript: b.ForceLocalChangesScript, reversePrAnalysis: b.ReversePrAnalysis, analysisId: b.AnalysisId, _volumes: b.Volumes, user: b.User, printProblems: b.PrintProblems, generateCodeClimateReport: b.GenerateCodeClimateReport, sendBitBucketInsights: b.SendBitBucketInsights, skipPull: b.SkipPull, fullHistory: b.FullHistory, applyFixes: b.ApplyFixes, cleanup: b.Cleanup, fixesStrategy: b.FixesStrategy, noStatistics: b.NoStatistics, cdnetSolution: b.CdnetSolution, cdnetProject: b.CdnetProject, cdnetConfiguration: b.CdnetConfiguration, cdnetPlatform: b.CdnetPlatform, cdnetNoBuild: b.CdnetNoBuild, clangCompileCommands: b.ClangCompileCommands, clangArgs: b.ClangArgs, analysisTimeoutMs: b.AnalysisTimeoutMs, analysisTimeoutExitCode: b.AnalysisTimeoutExitCode, jvmDebugPort: b.JvmDebugPort, globalConfigurationsDir: b.GlobalConfigurationsDir, globalConfigurationId: b.GlobalConfigurationId, customLocalQodanaYamlPath: b.CustomLocalQodanaYamlPath, qodanaYamlConfig: b.QodanaYamlConfig, } } func arrayCopy(arr []string) []string { newArr := make([]string, len(arr)) copy(newArr, arr) return newArr } func (c Context) StartHash() (string, error) { switch { case c.Commit() == c.DiffStart(): return c.Commit(), nil case c.Commit() == "": return c.DiffStart(), nil case c.DiffStart() == "": return c.Commit(), nil default: return "", fmt.Errorf("conflicting CLI arguments: --commit=%s --diff-start=%s", c.Commit(), c.DiffStart()) } } func (c Context) DetermineRunScenario(hasStartHash bool) RunScenario { if c.ForceLocalChangesScript() || c.Script() == "local-changes" { msg.WarningMessage("Using local-changes script is deprecated, please switch to other mechanisms of incremental analysis. Further information - https://www.jetbrains.com/help/qodana/analyze-pr.html") } switch { case c.FullHistory(): return RunScenarioFullHistory case !hasStartHash: return RunScenarioDefault case c.ForceLocalChangesScript(): return RunScenarioLocalChanges case c.analyser.IsContainer(): return RunScenarioDefault case c.ReversePrAnalysis(): return RunScenarioReversedScoped default: return RunScenarioScoped } } func (c Context) VmOptionsPath() string { return filepath.Join(c.ConfigDir(), "ide.vmoptions") } func (c Context) InstallPluginsVmOptionsPath() string { return filepath.Join(c.ConfigDir(), "install_plugins.vmoptions") } func (c Context) PropertiesAndFlags() (map[string]string, []string) { var flagsArr []string props := map[string]string{} for _, arg := range c.Property() { kv := strings.SplitN(arg, "=", 2) if len(kv) == 2 { props[kv[0]] = kv[1] } else { flagsArr = append(flagsArr, arg) } } return props, flagsArr } func (c Context) GetAnalysisTimeout() time.Duration { if c.AnalysisTimeoutMs() <= 0 { return time.Duration(math.MaxInt64) } return time.Duration(c.AnalysisTimeoutMs()) * time.Millisecond } func (c Context) LocalQodanaYamlExists() bool { path := qdyaml.GetLocalNotEffectiveQodanaYamlFullPath(c.ProjectDir(), c.CustomLocalQodanaYamlPath()) if path == "" { return false } info, _ := os.Stat(path) return info != nil } func (c Context) ProjectDirPathRelativeToRepositoryRoot() string { rootAbs, _ := filepath.Abs(c.RepositoryRoot()) projAbs, _ := filepath.Abs(c.ProjectDir()) rel, _ := filepath.Rel(rootAbs, projAbs) rel = filepath.ToSlash(rel) return rel } func IsScopedScenario(scenario string) bool { return scenario == RunScenarioScoped || scenario == RunScenarioReversedScoped }