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
}