internal/platform/qdyaml/yaml.go (260 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 qdyaml import ( "bytes" "errors" "os" "path/filepath" "sort" "strings" "github.com/JetBrains/qodana-cli/internal/platform/strutil" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) // QodanaYaml A standard qodana.yaml (or qodana.yml) format for Qodana configuration. // https://github.com/JetBrains/qodana-profiles/blob/master/schemas/qodana-yaml-1.0.json type QodanaYaml struct { // The qodana.yaml version of this log file. Version string `yaml:"version,omitempty"` // Profile is the profile configuration for Qodana analysis (either a profile name or a profile path). Profile Profile `yaml:"profile,omitempty"` // FailThreshold is a number of problems to fail the analysis (to exit from Qodana with code 255). FailThreshold *int `yaml:"failThreshold,omitempty"` // Script is the run scenario. 'default' by default Script Script `yaml:"script,omitempty"` // Clude property to disable the wanted checks on the wanted paths. Excludes []Clude `yaml:"exclude,omitempty"` // Include property to enable the wanted checks. Includes []Clude `yaml:"include,omitempty"` // Linter to run. Linter string `yaml:"linter,omitempty"` // Image to use. Image string `yaml:"image,omitempty"` //WithinDocker defines if analysis should be performed in a container. WithinDocker string `yaml:"withinDocker,omitempty"` // IDE to run. Ide string `yaml:"ide,omitempty"` // Bootstrap contains a command to run in the container before the analysis starts. Bootstrap string `yaml:"bootstrap,omitempty"` // Properties property to override IDE properties. Properties map[string]string `yaml:"properties,omitempty"` // LicenseRules contains a list of license rules to apply for license checks. LicenseRules []LicenseRule `yaml:"licenseRules,omitempty"` // DependencyIgnores contains a list of dependencies to ignore for license checks in Qodana. DependencyIgnores []DependencyIgnore `yaml:"dependencyIgnores,omitempty"` // DependencyOverrides contains a list of dependencies metadata to override for license checks in Qodana. DependencyOverrides []DependencyOverride `yaml:"dependencyOverrides,omitempty"` // Overrides the licenses attached to the project ProjectLicenses []LicenseOverride `yaml:"projectLicenses,omitempty"` // CustomDependencies contains a list of custom dependencies to add to license checks in Qodana. CustomDependencies []CustomDependency `yaml:"customDependencies,omitempty"` // Plugins property containing plugins to install. Plugins []Plugin `yaml:"plugins,omitempty"` // DotNet is the configuration for .NET solutions and projects (either a solution name or a project name). DotNet DotNet `yaml:"dotnet,omitempty"` // ProjectJdk is the configuration for the project JDK. ProjectJdk string `yaml:"projectJDK,omitempty"` // Php is the configuration for PHP projects. Php Php `yaml:"php,omitempty"` // DisableSanityInspections property to disable sanity inspections. DisableSanityInspections string `yaml:"disableSanityInspections,omitempty"` // FixesStrategy property to set fixes strategy. Can be none (default), apply, cleanup. FixesStrategy string `yaml:"fixesStrategy,omitempty"` // RunPromoInspections property to run promo inspections. RunPromoInspections string `yaml:"runPromoInspections,omitempty"` // IncludeAbsent property to include absent problems from baseline. IncludeAbsent string `yaml:"includeAbsent,omitempty"` // MaxRuntimeNotifications property defines maximum amount of internal errors to collect in the report MaxRuntimeNotifications int `yaml:"maxRuntimeNotifications,omitempty"` // FailOnErrorNotification property defines whether to fail the run when any internal error was encountered. In that case, the program returns exit code 70 FailOnErrorNotification bool `yaml:"failOnErrorNotification,omitempty"` // FailureConditions configures individual failure conditions. Absent properties will not be checked FailureConditions FailureConditions `yaml:"failureConditions,omitempty"` // DependencySbomExclude property to define which dependencies to exclude from the generated SBOM report DependencySbomExclude []DependencyIgnore `yaml:"dependencySbomExclude,omitempty"` // ModulesToAnalyze property to define which submodules to include. Omitting this key will include all submodules. ModulesToAnalyze []ModuleToAnalyze `yaml:"modulesToAnalyze,omitempty"` // AnalyzeDevDependencies property whether to include dev dependencies in the analysis AnalyzeDevDependencies bool `yaml:"analyzeDevDependencies,omitempty"` // EnablePackageSearch property to start using package-search service for fetching license data for dependencies (only for jvm libraries) EnablePackageSearch bool `yaml:"enablePackageSearch,omitempty"` // RaiseLicenseProblems property to show license problems like other inspections. RaiseLicenseProblems bool `yaml:"raiseLicenseProblems,omitempty"` } // WriteConfig writes QodanaYaml to the given path. func (q *QodanaYaml) WriteConfig(path string) error { var b bytes.Buffer yamlEncoder := yaml.NewEncoder(&b) yamlEncoder.SetIndent(2) err := yamlEncoder.Encode(&q) if err != nil { return err } err = os.WriteFile(path, b.Bytes(), 0o600) if err != nil { log.Fatalf("Marshal: %v", err) } return nil } const warningComment = `################################################################################# # WARNING: Do not store sensitive information in this file, # # as its contents will be included in the Qodana report. # ################################################################################# ` // WriteConfigWithWarning writes QodanaYaml with warning to the given path func (q *QodanaYaml) WriteConfigWithWarning(path string) error { var b bytes.Buffer yamlEncoder := yaml.NewEncoder(&b) yamlEncoder.SetIndent(2) err := yamlEncoder.Encode(&q) if err != nil { return err } out := append([]byte(warningComment), b.Bytes()...) err = os.WriteFile(path, out, 0o600) if err != nil { log.Fatalf("Marshal: %v", err) } return nil } // Profile A profile is some template set of checks to run with Qodana analysis. // //goland:noinspection GoUnnecessarilyExportedIdentifiers type Profile struct { // Name profile name to use. Name string `yaml:"name,omitempty"` // Path profile path to use. Path string `yaml:"path,omitempty"` } // Clude A check id to enable/disable for include/exclude YAML field. // //goland:noinspection GoUnnecessarilyExportedIdentifiers type Clude struct { // The name of check to include/exclude. Name string `yaml:"name"` // Relative to the project root path to enable/disable analysis. Paths []string `yaml:"paths,omitempty"` } // Plugin to be installed during the Qodana run. // //goland:noinspection GoUnnecessarilyExportedIdentifiers type Plugin struct { // Id plugin id to install. Id string `yaml:"id"` } // DependencyIgnore is a dependency to ignore for license checks in Qodana // //goland:noinspection GoUnnecessarilyExportedIdentifiers type DependencyIgnore struct { // Name is the name of the dependency to ignore. Name string `yaml:"name"` } // LicenseRule is a license rule to apply for license compatibility checks in Qodana // //goland:noinspection GoUnnecessarilyExportedIdentifiers type LicenseRule struct { // Keys is the list of project license SPDX IDs. Keys []string `yaml:"keys"` // Allowed is the list of allowed dependency licenses for project licenses. Allowed []string `yaml:"allowed,omitempty"` // Prohibited is the list of prohibited dependency licenses for project licenses. Prohibited []string `yaml:"prohibited,omitempty"` } // ModuleToAnalyze is a submodule to include in the analysis // //goland:noinspection GoUnnecessarilyExportedIdentifiers type ModuleToAnalyze struct { // Name corresponds to the JSON schema field "name". Name *string `yaml:"name,omitempty"` } //goland:noinspection GoUnnecessarilyExportedIdentifiers type DependencyOverride struct { // Name is dependency name. Name string `yaml:"name"` // Version is the dependency version. Version string `yaml:"version"` // Url is the dependency URL. Url string `yaml:"url,omitempty"` // LicenseOverride is the license of the dependency. Licenses []LicenseOverride `yaml:"licenses"` } //goland:noinspection GoUnnecessarilyExportedIdentifiers type LicenseOverride struct { // Key is the SPDX ID of the license. Key string `yaml:"key"` // Url is the URL of the license. Url string `yaml:"url,omitempty"` } //goland:noinspection GoUnnecessarilyExportedIdentifiers type CustomDependency struct { // Name is the name of the dependency. Name string `yaml:"name"` // Version is the dependency version. Version string `yaml:"version"` // Url is the dependency URL. Url string `yaml:"url,omitempty"` // LicenseOverride is the license of the dependency. Licenses []LicenseOverride `yaml:"licenses"` } //goland:noinspection GoUnnecessarilyExportedIdentifiers type DotNet struct { // Solution is the name of a .NET solution inside the Qodana project. Solution string `yaml:"solution,omitempty"` // Project is the name of a .NET project inside the Qodana project. Project string `yaml:"project,omitempty"` // Configuration is the configuration in which .NET project should be opened by Qodana. Configuration string `yaml:"configuration,omitempty"` // Platform is the target platform in which .NET project should be opened by Qodana. Platform string `yaml:"platform,omitempty"` // Frameworks is a semicolon-separated list of target framework monikers (TFM) to be analyzed. Frameworks string `yaml:"frameworks,omitempty"` } type FailureConditions struct { // SeverityThresholds corresponds to the JSON schema field "severityThresholds". SeverityThresholds *SeverityThresholds `yaml:"severityThresholds,omitempty"` // TestCoverageThresholds corresponds to the JSON schema field // "testCoverageThresholds". TestCoverageThresholds *CoverageThresholds `yaml:"testCoverageThresholds,omitempty"` } // SeverityThresholds Configures maximum thresholds for different problem severities. Absent properties are not checked. If a baseline is given, only new results are counted // //goland:noinspection GoUnnecessarilyExportedIdentifiers type SeverityThresholds struct { // The run fails if the total amount of results exceeds this number. Any *int `yaml:"any,omitempty"` // The run fails if the amount results with severity CRITICAL exceeds this number. Critical *int `yaml:"critical,omitempty"` // The run fails if the amount results with severity HIGH exceeds this number. High *int `yaml:"high,omitempty"` // The run fails if the amount results with severity INFO exceeds this number. Info *int `yaml:"info,omitempty"` // The run fails if the amount results with severity LOW exceeds this number. Low *int `yaml:"low,omitempty"` // The run fails if the amount results with severity MODERATE exceeds this number. Moderate *int `yaml:"moderate,omitempty"` } // CoverageThresholds Configures minimum thresholds for test coverage metrics. Absent properties are not checked // //goland:noinspection GoUnnecessarilyExportedIdentifiers type CoverageThresholds struct { // The run fails if the percentage of fresh lines covered is lower than this // number Fresh *int `json:"fresh,omitempty" yaml:"fresh,omitempty" mapstructure:"fresh,omitempty"` // The run fails if the percentage of total lines covered is lower than this // number. Total *int `json:"total,omitempty" yaml:"total,omitempty" mapstructure:"total,omitempty"` } type Script struct { Name string `yaml:"name,omitempty"` Parameters map[string]interface{} `yaml:"parameters,omitempty"` } // IsEmpty checks whether the .NET configuration is empty or not. func (d DotNet) IsEmpty() bool { return d.Solution == "" && d.Project == "" } //goland:noinspection GoUnnecessarilyExportedIdentifiers type Php struct { // Version is the PHP version to use for the analysis. Version string `yaml:"version,omitempty"` } func defaultLocalNotEffectiveQodanaYamlIfExists(project string) string { filenames := []string{"qodana.yml", "qodana.yaml"} for _, filename := range filenames { if info, _ := os.Stat(filepath.Join(project, filename)); info != nil { return filename } } return "" } // GetLocalNotEffectiveQodanaYamlFullPath does not process `imports` field and doesn't use global configuration func GetLocalNotEffectiveQodanaYamlFullPath(project string, yamlPathInProject string) string { if filepath.IsAbs(yamlPathInProject) { return yamlPathInProject } if yamlPathInProject == "" { yamlPathInProject = defaultLocalNotEffectiveQodanaYamlIfExists(project) } if yamlPathInProject == "" { return "" } return filepath.Join(project, yamlPathInProject) } // TestOnlyLoadLocalNotEffectiveQodanaYaml test only! // Gets Qodana YAML from the project. Does not process `imports` field and doesn't use global configurations func TestOnlyLoadLocalNotEffectiveQodanaYaml(project string, yamlPathInProject string) QodanaYaml { qodanaYamlPath := GetLocalNotEffectiveQodanaYamlFullPath(project, yamlPathInProject) return LoadQodanaYamlByFullPath(qodanaYamlPath) } func LoadQodanaYamlByFullPath(fullPath string) QodanaYaml { if fullPath == "" { return QodanaYaml{} } q := &QodanaYaml{} if _, err := os.Stat(fullPath); errors.Is(err, os.ErrNotExist) { return *q } yamlFile, err := os.ReadFile(fullPath) if err != nil { log.Printf("yamlFile.Get err #%v ", err) } err = yaml.Unmarshal(yamlFile, q) if err != nil { log.Fatalf("Unmarshal: %v", err) } return *q } // Sort makes QodanaYaml prettier. func (q *QodanaYaml) Sort() *QodanaYaml { sort.Slice( q.Includes, func(i, j int) bool { return strutil.Lower(q.Includes[i].Name) < strutil.Lower(q.Includes[j].Name) }, ) sort.Slice( q.Excludes, func(i, j int) bool { return strutil.Lower(q.Excludes[i].Name) < strutil.Lower(q.Excludes[j].Name) }, ) for _, rule := range q.LicenseRules { sort.Slice( rule.Keys, func(i, j int) bool { return strutil.Lower(rule.Keys[i]) < strutil.Lower(rule.Keys[j]) }, ) sort.Slice( rule.Allowed, func(i, j int) bool { return strutil.Lower(rule.Allowed[i]) < strutil.Lower(rule.Allowed[j]) }, ) sort.Slice( rule.Prohibited, func(i, j int) bool { return strutil.Lower(rule.Prohibited[i]) < strutil.Lower(rule.Prohibited[j]) }, ) } sort.Slice( q.DependencyIgnores, func(i, j int) bool { return strutil.Lower(q.DependencyIgnores[i].Name) < strutil.Lower(q.DependencyIgnores[j].Name) }, ) sort.Slice( q.DependencyOverrides, func(i, j int) bool { return strutil.Lower(q.DependencyOverrides[i].Name) < strutil.Lower(q.DependencyOverrides[j].Name) }, ) sort.Slice( q.CustomDependencies, func(i, j int) bool { return strutil.Lower(q.CustomDependencies[i].Name) < strutil.Lower(q.CustomDependencies[j].Name) }, ) sort.Slice( q.Plugins, func(i, j int) bool { return strutil.Lower(q.Plugins[i].Id) < strutil.Lower(q.Plugins[j].Id) }, ) return q } func (q *QodanaYaml) IsDotNet() bool { return strings.Contains(q.Linter, "dotnet") || strings.Contains(q.Linter, "cdnet") || strings.Contains( q.Ide, "QDNET", ) } // SetQodanaDotNet adds the .NET configuration to the qodana.yaml file. func SetQodanaDotNet(qodanaYamlFullPath string, dotNet *DotNet) bool { q := LoadQodanaYamlByFullPath(qodanaYamlFullPath) q.DotNet = *dotNet err := q.WriteConfig(qodanaYamlFullPath) if err != nil { log.Fatalf("writeConfig: %v", err) } return true }