internal/platform/commoncontext/compute.go (261 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 commoncontext import ( "crypto/sha256" "encoding/hex" "fmt" "os" "path/filepath" "github.com/JetBrains/qodana-cli/internal/platform/git" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/product" "github.com/JetBrains/qodana-cli/internal/platform/qdcontainer" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" "github.com/JetBrains/qodana-cli/internal/platform/qdyaml" log "github.com/sirupsen/logrus" ) func Compute( overrideLinter string, overrideIde string, overrideImage string, overrideWithinDocker string, cacheDirFromCliOptions string, resultsDirFromCliOptions string, reportDirFromCliOptions string, qodanaCloudToken string, clearCache bool, projectDir string, repositoryRoot string, localNotEffectiveQodanaYamlPathInProject string, ) Context { analyzer := GuessAnalyzerFromEnvAndCLI(overrideIde, overrideLinter, overrideImage, overrideWithinDocker) if analyzer == nil { analyzer = getAnalyzerFromProject( qodanaCloudToken, projectDir, localNotEffectiveQodanaYamlPathInProject, ) } return computeCommon( analyzer, projectDir, repositoryRoot, cacheDirFromCliOptions, resultsDirFromCliOptions, reportDirFromCliOptions, clearCache, qodanaCloudToken, ) } func Compute3rdParty( linterName string, isEap bool, cacheDirFromCliOptions string, resultsDirFromCliOptions string, reportDirFromCliOptions string, qodanaCloudToken string, clearCache bool, projectDir string, repositoryRoot string, ) Context { linter := product.FindLinterByName(linterName) if linter == product.UnknownLinter { log.Fatalf("Unsupported 3rd party linter name detected: %s", linterName) } analyzer := &product.NativeAnalyzer{ Linter: linter, Eap: isEap, } return computeCommon( analyzer, projectDir, repositoryRoot, cacheDirFromCliOptions, resultsDirFromCliOptions, reportDirFromCliOptions, clearCache, qodanaCloudToken, ) } func computeCommon( analyzer product.Analyzer, projectDir string, repositoryRoot string, cacheDirFromCliOptions string, resultsDirFromCliOptions string, reportDirFromCliOptions string, clearCache bool, qodanaCloudToken string, ) Context { qodanaId := computeId(analyzer, projectDir) systemDir := computeQodanaSystemDir(cacheDirFromCliOptions) linterDir := filepath.Join(systemDir, qodanaId) resultsDir := computeResultsDir(resultsDirFromCliOptions, linterDir) cacheDir := computeCacheDir(cacheDirFromCliOptions, linterDir) reportDir := computeReportDir(reportDirFromCliOptions, resultsDir) commonCtx := Context{ Analyzer: analyzer, IsClearCache: clearCache, CacheDir: cacheDir, ResultsDir: resultsDir, QodanaSystemDir: systemDir, ReportDir: reportDir, Id: qodanaId, QodanaToken: qodanaCloudToken, } if repositoryRoot == "" { vcsRoot, err := git.Root(projectDir, commonCtx.LogDir()) if err != nil { repositoryRoot = projectDir } else { repositoryRoot = vcsRoot } } normalizedProjectDir, err := normalizePath(projectDir) if err != nil { log.Fatalf("Can not normalize project dir %s: %v", projectDir, err) } // Normalize repositoryRoot to be a substring of projectDir path // This handles case-insensitive filesystems where /tmp/PROJECT and /tmp/project are the same normalizedRepoRoot, err := normalizeRepositoryRoot(normalizedProjectDir, repositoryRoot) if err != nil { log.Fatalf( "The project directory must be located inside repository root. Please, specify correct --repository-root argument. ProjectDir: %s. RepositoryRoot: %s. Error: %v", normalizedProjectDir, normalizedRepoRoot, err, ) } log.Debugf("Repository root: %q", repositoryRoot) log.Debugf("Normalized repository root: %q", normalizedRepoRoot) log.Debugf("Project root: %q", projectDir) log.Debugf("Normalized project root: %q", normalizedProjectDir) commonCtx.ProjectDir = normalizedProjectDir commonCtx.RepositoryRoot = normalizedRepoRoot return commonCtx } // normalizeRepositoryRoot checks if projectDir is inside or equal to repositoryRoot and returns // the repositoryRoot path as it appears in projectDir (to handle case-insensitive filesystems). // Returns error if projectDir is not inside repositoryRoot. func normalizeRepositoryRoot(projectDir, repositoryRoot string) (string, error) { normalizedRepoRoot, err := normalizePath(repositoryRoot) if err != nil { return repositoryRoot, err } repoRootInfo, err := os.Stat(normalizedRepoRoot) if err != nil { return repositoryRoot, err } // Walk up from projectDir to find the directory that matches repositoryRoot // Return the path as it appears in projectDir's path current := projectDir for { currentInfo, err := os.Stat(current) if err != nil { return repositoryRoot, err } if os.SameFile(currentInfo, repoRootInfo) { // Found the matching directory - return current path (from projectDir's perspective) return current, nil } parent := filepath.Dir(current) if parent == current { // Reached root without finding repositoryRoot return normalizedRepoRoot, fmt.Errorf("projectDir is not inside repositoryRoot") } current = parent } } func normalizePath(path string) (string, error) { pathWithoutSymlinks, err := filepath.EvalSymlinks(path) if err != nil { return "", err } return filepath.Abs(pathWithoutSymlinks) } func getAnalyzerFromProject( qodanaCloudToken string, projectDir string, localNotEffectiveQodanaYamlPathInProject string, ) product.Analyzer { qodanaYamlPath := qdyaml.GetLocalNotEffectiveQodanaYamlFullPath( projectDir, localNotEffectiveQodanaYamlPathInProject, ) qodanaYaml := qdyaml.LoadQodanaYamlByFullPath(qodanaYamlPath) if qodanaYaml.Linter == "" && qodanaYaml.Ide == "" && qodanaYaml.Image == "" { msg.WarningMessage( "No valid `linter:` or `image:` field found in %s. Have you run %s? Running that for you...", msg.PrimaryBold(localNotEffectiveQodanaYamlPathInProject), msg.PrimaryBold("qodana init"), ) return SelectAnalyzerForPath(projectDir, qodanaCloudToken) } if qodanaYaml.Ide != "" { msg.WarningMessage( "`ide:` field in %s is deprecated. Please use `--linter` and `--within-docker=false` instead.", qodanaYamlPath, ) } if qodanaYaml.Linter != "" && qodanaYaml.Ide != "" { log.Fatalf( "You have both `linter:` (%s) and `ide:` (%s) fields set in %s. Modify the configuration file to keep one of them", qodanaYaml.Linter, qodanaYaml.Ide, qodanaYamlPath, ) return nil } if qodanaYaml.Image != "" && qodanaYaml.Ide != "" { log.Fatalf( "You have both `image:` (%s) and `ide:` (%s) fields set in %s. Modify the configuration file to keep one of them", qodanaYaml.Image, qodanaYaml.Ide, localNotEffectiveQodanaYamlPathInProject, ) return nil } return guessAnalyzerFromParams(qodanaYaml.Ide, qodanaYaml.Linter, qodanaYaml.Image, qodanaYaml.WithinDocker) } func computeId(analyzer product.Analyzer, projectDir string) string { length := 7 projectAbs, _ := filepath.Abs(projectDir) id := fmt.Sprintf( "%s-%s", getHash(analyzer.Name())[0:length+1], getHash(projectAbs)[0:length+1], ) return id } // getHash returns a SHA256 hash of a given string. func getHash(s string) string { sha256sum := sha256.Sum256([]byte(s)) return hex.EncodeToString(sha256sum[:]) } func computeQodanaSystemDir(cacheDirFromCliOptions string) string { if cacheDirFromCliOptions != "" { return filepath.Dir(filepath.Dir(cacheDirFromCliOptions)) } userCacheDir, _ := os.UserCacheDir() return filepath.Join( userCacheDir, "JetBrains", "Qodana", ) } func computeResultsDir(resultsDirFromCliOptions string, linterDir string) string { if resultsDirFromCliOptions != "" { return resultsDirFromCliOptions } if qdenv.IsContainer() { return qdcontainer.DataResultsDir } return filepath.Join(linterDir, "results") } func computeCacheDir(cacheDirFromCliOptions string, linterDir string) string { if cacheDirFromCliOptions != "" { return cacheDirFromCliOptions } if qdenv.IsContainer() { return qdcontainer.DataCacheDir } return filepath.Join(linterDir, "cache") } func computeReportDir(reportDirFromCliOptions string, resultsDir string) string { if reportDirFromCliOptions != "" { return reportDirFromCliOptions } if qdenv.IsContainer() { return qdcontainer.DataResultsReportDir } return filepath.Join(resultsDir, "report") }