internal/platform/commoncontext/common.go (360 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 ( "fmt" "net/http" "os" "path/filepath" "slices" "strings" "time" "github.com/JetBrains/qodana-cli/internal/cloud" "github.com/JetBrains/qodana-cli/internal/platform/algorithm" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/product" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" "github.com/JetBrains/qodana-cli/internal/platform/qdyaml" "github.com/JetBrains/qodana-cli/internal/platform/utils" "github.com/pterm/pterm" log "github.com/sirupsen/logrus" ) func GuessAnalyzerFromEnvAndCLI( ide string, linter string, image string, withinDocker string, ) product.Analyzer { dist, exists := os.LookupEnv(qdenv.QodanaDistEnv) if exists && dist != "" { analyzer, err := BuildPathNativeAnalyzer(dist) if err != nil { log.Fatalf("Env %s doesn't point to valid distribution: %s", qdenv.QodanaDistEnv, err) } return analyzer } if linter != "" && ide != "" { log.Fatalf( "You have both `--linter:` (%s) and `--ide:` (%s) specified. Please remove --ide it's deprecated.", linter, ide, ) return nil } if image != "" && ide != "" { log.Fatalf( "You have both `--image:` (%s) and `--ide:` (%s) specified. Please remove --ide it's deprecated.", image, ide, ) return nil } return guessAnalyzerFromParams(ide, linter, image, withinDocker) } func guessAnalyzerFromParams( ide string, linterParam string, image string, withinDocker string, ) product.Analyzer { if ide != "" { linter := product.FindLinterByProductCode(ide) if linter == product.UnknownLinter { //legacy support log.Warnf( "--ide value %s is not recognised as product code, trying to interpret as path to distribution\n", ide, ) analyzer, err := BuildPathNativeAnalyzer(ide) if err != nil { log.Fatalf("Flag --ide doesn't point to valid distribution: %s", err) } return analyzer } return &product.NativeAnalyzer{ Linter: linter, Eap: strings.Contains(ide, product.EapSuffix), } } if image != "" { return &product.DockerAnalyzer{ Linter: product.FindLinterByImage(image), Image: image, } } if linterParam != "" { return GuessAnalyzerByLinterParam(linterParam, withinDocker) } return nil } func GuessAnalyzerByLinterParam(linterParam string, withinDocker string) product.Analyzer { linter := product.FindLinterByName(linterParam) if linter == product.UnknownLinter { //legacy support - linter param with image values linterLikeImage := product.FindLinterByImage(linterParam) if linterLikeImage != product.UnknownLinter { msg.WarningMessage("Linter %s has image value, please use --image param\n", linterParam) return &product.DockerAnalyzer{ Linter: linterLikeImage, Image: linterParam, } } log.Fatalf( "Unrecognized '--linter' value '%s'. Hint: If the provided value is a custom doсker image, please use '--image' instead.", linterParam, ) return nil } withinContainerLower := strings.ToLower(withinDocker) if withinDocker == "" || withinContainerLower == "true" { return &product.DockerAnalyzer{ Linter: linter, Image: linter.Image(), } } else if withinContainerLower == "false" { //goland:noinspection GoBoolExpressions return &product.NativeAnalyzer{ Linter: linter, Eap: !product.IsReleased || linter.EapOnly, } } log.Fatalf("Wrong value for within-docker param: %s. Use true/false.", withinDocker) return nil } func BuildPathNativeAnalyzer(dist string) (product.Analyzer, error) { productInfo, err := product.ReadIdeProductInfo(dist) if err != nil { return nil, fmt.Errorf("can't read product-info.json: %v ", err) } flavourProductCode := product.ReadDistFlavour(dist) if flavourProductCode != "" { linter := product.FindLinterByProductCode(flavourProductCode) if linter == product.UnknownLinter { log.Fatalf("Unknown product code %s", flavourProductCode) } return &product.PathNativeAnalyzer{ Linter: linter, Path: dist, IsEap: product.IsEap(productInfo), }, nil } info, err := product.FindLinterPropertiesByProductInfo(productInfo.ProductCode) if err != nil { return nil, fmt.Errorf("product dist %s is not recognised as valid: %v", dist, err) } return &product.PathNativeAnalyzer{ Linter: info.Linter, Path: dist, IsEap: product.IsEap(productInfo), }, nil } // SelectAnalyzerForPath gets linter for the given path func SelectAnalyzerForPath(path string, token string) product.Analyzer { var linters []product.Linter msg.PrintProcess( func(_ *pterm.SpinnerPrinter) { languages := readIdeaDir(path) if len(languages) == 0 { languages, _ = recognizeDirLanguages(path) } if len(languages) == 0 { msg.WarningMessage("No technologies detected (no source code files?)\n") } else { msg.WarningMessage("Detected technologies: " + strings.Join(languages, ", ") + "\n") for _, language := range languages { if i, ok := product.LangsToLinters[language]; ok { linters = append(linters, i...) } } if len(linters) == 0 { linters = product.AllLinters } } // breaking change will not be backported to 241 if (slices.Contains(linters, product.AndroidCommunityLinter) || slices.Contains(linters, product.AndroidLinter)) && isAndroidProject(path) { filteredLinters := make([]product.Linter, 0, len(linters)) for _, l := range linters { if l != product.AndroidLinter && l != product.AndroidCommunityLinter { filteredLinters = append(filteredLinters, l) } } linters = append( []product.Linter{product.AndroidLinter, product.AndroidCommunityLinter}, filteredLinters..., ) } }, "Scanning project", "", ) linters = algorithm.Unique(linters) selector := func(choices []string) string { choice, err := msg.QodanaInteractiveSelect.WithOptions(choices).Show() if err != nil { msg.ErrorMessage("%s", err) return "" } return choice } interactive := msg.IsInteractive() linters = filterByLicensePlan(linters, token) analyzer := selectAnalyzer(path, linters, interactive, selector) if analyzer == nil { msg.ErrorMessage("Could not configure project as it is not supported by Qodana") msg.WarningMessage("See https://www.jetbrains.com/help/qodana/supported-technologies.html for more details") os.Exit(1) } msg.SuccessMessage("Selected '%s'", analyzer.GetLinter().PresentableName) return analyzer } func filterByLicensePlan(linters []product.Linter, token string) []product.Linter { if token == "" { return linters } cloud.SetupLicenseToken(token) if licensePlan := cloud.GetCloudApiEndpoints().GetLicensePlan(token); licensePlan == cloud.CommunityLicensePlan { var filteredCodes []product.Linter for _, linter := range linters { if !linter.IsPaid { filteredCodes = append(filteredCodes, linter) } } return filteredCodes } return linters } // GetAndSaveDotNetConfig gets .NET config for the given path and saves configName func GetAndSaveDotNetConfig(projectDir string, qodanaYamlFullPath string) bool { possibleOptions := utils.FindFiles(projectDir, []string{".sln", ".csproj", ".vbproj", ".fsproj"}) if len(possibleOptions) <= 1 { return false } msg.WarningMessage("Detected multiple .NET solution/project files, select the preferred one \n") choice, err := msg.QodanaInteractiveSelect.WithOptions(possibleOptions).WithDefaultText("Select solution/project").Show() if err != nil { msg.ErrorMessage("%s", err) return false } dotnet := &qdyaml.DotNet{} if strings.HasSuffix(choice, ".sln") { dotnet.Solution = filepath.Base(choice) } else { dotnet.Project = filepath.Base(choice) } return qdyaml.SetQodanaDotNet(qodanaYamlFullPath, dotnet) } func selectAnalyzer( path string, linters []product.Linter, interactive bool, selectFunc func([]string) string, ) product.Analyzer { var distribution product.Analyzer if len(linters) == 0 && !interactive { return nil } selection, choices := analyzerToSelect(linters, path) log.Debugf("Detected products: %s", strings.Join(choices, ", ")) if len(choices) == 1 || !interactive { distribution = selection[choices[0]] } else { choice := selectFunc(choices) if choice == "" { return nil } distribution = selection[choice] } return distribution } func analyzerToSelect(linters []product.Linter, path string) (map[string]product.Analyzer, []string) { analyzersMap := make(map[string]product.Analyzer) analyzersList := make([]string, 0, len(linters)) for _, linter := range linters { if !isNativeRequired(path, linter) { dockerKey := linter.PresentableName + " (Docker)" analyzersMap[dockerKey] = &product.DockerAnalyzer{ Linter: linter, Image: linter.Image(), } analyzersList = append(analyzersList, dockerKey) } if linter.SupportNative { key := linter.PresentableName + " (Native)" analyzersMap[key] = &product.NativeAnalyzer{ Linter: linter, Eap: linter.EapOnly, } analyzersList = append(analyzersList, key) } } return analyzersMap, analyzersList } // ShowReport serves the Qodana report func ShowReport(resultsDir string, reportPath string, port int) { cloudUrl := cloud.GetReportUrl(resultsDir) if cloudUrl != "" { openReport(cloudUrl, reportPath, port) } else { msg.WarningMessage("Press Ctrl+C to stop serving the report\n") msg.PrintProcess( func(_ *pterm.SpinnerPrinter) { if _, err := os.Stat(reportPath); os.IsNotExist(err) { log.Fatal("Qodana report not found. Get a report by running `qodana scan`") } openReport("", reportPath, port) }, fmt.Sprintf("Showing Qodana report from %s", fmt.Sprintf("http://localhost:%d/", port)), "", ) } } // openReport serves the report on the given port and opens the browser. func openReport(cloudUrl string, path string, port int) { if cloudUrl != "" { resp, err := http.Get(cloudUrl) if err == nil && resp.StatusCode == 200 { err = utils.OpenBrowser(cloudUrl) if err != nil { return } } return } url := fmt.Sprintf("http://localhost:%d", port) go func() { resp, err := http.Get(url) if err == nil && resp.StatusCode == 200 { err := utils.OpenBrowser(url) if err != nil { return } } }() http.Handle("/", noCache(http.FileServer(http.Dir(path)))) err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) if err != nil { msg.WarningMessage("Problem serving report, %s\n", err.Error()) return } _, _ = fmt.Scan() } // noCache handles serving the static files with no cache headers. func noCache(h http.Handler) http.Handler { etagHeaders := []string{ "ETag", "If-Modified-Since", "If-Match", "If-None-Match", "If-Range", "If-Unmodified-Since", } epoch := time.Unix(0, 0).Format(time.RFC1123) noCacheHeaders := map[string]string{ "Expires": epoch, "Cache-Control": "no-cache, private, max-age=0", "Pragma": "no-cache", "X-Accel-Expires": "0", } fn := func(w http.ResponseWriter, r *http.Request) { for _, x := range etagHeaders { if r.Header.Get(x) != "" { r.Header.Del(x) } } for k, v := range noCacheHeaders { w.Header().Set(k, v) } h.ServeHTTP(w, r) } return http.HandlerFunc(fn) } var InterruptChannel chan os.Signal