Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion messages/config-command.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# command.summary

Display the current state of configuration for Code Analyzer.
Output the current state of configuration for Code Analyzer.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the command is for either displaying to the terminal or writing to a file. I think the world "Output" makes more sense here.


# command.description

Expand Down Expand Up @@ -28,6 +28,10 @@ We're continually improving Salesforce Code Analyzer. Tell us what you think! Gi

<%= config.bin %> <%= command.id %> --rule-selector Recommended

- By default, only rule override values that you have specified in your `code-analyzer.yml` file that are not default values are displayed. To display the default rule values, in addition to the modified values, for the recommended rules:

<%= config.bin %> <%= command.id %> --rule-selector Recommended --include-unmodified-rules

- Display the configuration state associated with all the rules that are applicable to the files targeted within the folder `./src`:

<%= config.bin %> <%= command.id %> --target ./src
Expand Down Expand Up @@ -101,3 +105,11 @@ Output file to write the configuration state to. The file is written in YAML for
If you specify a file within folder, such as `--output-file ./config/code-analyzer.yml`, the folder must already exist, or you get an error. If the file already exists, a prompt asks if you want to overwrite it.

If you don't specify this flag, the command outputs the configuration state to the terminal.

# flags.include-unmodified-rules.summary

Includes unmodified rules in the rule override settings.

# flags.include-unmodified-rules.description

The default behavior of the config command is to not include the unmodified rules with their default values in the rule override settings (for the rules selected via the `–-rule-selector` flag). This helps prevent your configuration file from being unnecessarily large. If you wish to instead include the unmodified rules, in addition to the modified rules, then specify this flag.
4 changes: 2 additions & 2 deletions messages/config-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Modified from: %s
# template.rule-overrides-section
%s ENGINE RULE OVERRIDES

# template.yaml.no-engines-selected
# template.yaml.no-rules-selected
Empty object used because rule selection returned no rules

# template.yaml.no-rules-selected
# template.yaml.no-rule-overrides
Remove this empty object {} when you are ready to specify your first rule override

# template.common.end-of-config
Expand Down
4 changes: 4 additions & 0 deletions src/commands/code-analyzer/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export default class ConfigCommand extends SfCommand<void> implements Displayabl
summary: getMessage(BundleName.ConfigCommand, 'flags.output-file.summary'),
description: getMessage(BundleName.ConfigCommand, 'flags.output-file.description'),
char: 'f'
}),
'include-unmodified-rules': Flags.boolean({
summary: getMessage(BundleName.ConfigCommand, 'flags.include-unmodified-rules.summary'),
description: getMessage(BundleName.ConfigCommand, 'flags.include-unmodified-rules.description')
})
};

Expand Down
4 changes: 3 additions & 1 deletion src/lib/actions/ConfigAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type ConfigInput = {
'rule-selector': string[];
workspace?: string[];
target?: string[];
'include-unmodified-rules'?: boolean
};

export class ConfigAction {
Expand Down Expand Up @@ -137,7 +138,8 @@ export class ConfigAction {
...userRules.getEngineNames(),
...selectedDefaultRules.getEngineNames()]);

const configModel: ConfigModel = new AnnotatedConfigModel(userCore, userRules, allDefaultRules, relevantEngines);
const includeUnmodifiedRules: boolean = input["include-unmodified-rules"] ?? false;
const configModel: ConfigModel = new AnnotatedConfigModel(userCore, userRules, allDefaultRules, relevantEngines, includeUnmodifiedRules);

const fileWritten: boolean = this.dependencies.writer
? await this.dependencies.writer.write(configModel)
Expand Down
74 changes: 47 additions & 27 deletions src/lib/models/ConfigModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ export class AnnotatedConfigModel implements ConfigModel {
// configs not associated with the user's rule selection, thus we can't use the engines from allDefaultRules.
private readonly relevantEngines: Set<string>;

constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>) {
private readonly includeUnmodifiedRules: boolean;

constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>, includeUnmodifiedRules: boolean) {
this.codeAnalyzer = codeAnalyzer;
this.userRules = userRules;
this.allDefaultRules = allDefaultRules;
this.relevantEngines = relevantEngines;
this.includeUnmodifiedRules = includeUnmodifiedRules;
}

toFormattedOutput(format: OutputFormat): string {
// istanbul ignore else: Should be impossible
if (format === OutputFormat.STYLED_YAML) {
return new StyledYamlFormatter(this.codeAnalyzer, this.userRules, this.allDefaultRules, this.relevantEngines).toYaml();
return new StyledYamlFormatter(this.codeAnalyzer, this.userRules, this.allDefaultRules, this.relevantEngines, this.includeUnmodifiedRules).toYaml();
} else if (format === OutputFormat.RAW_YAML) {
return new PlainYamlFormatter(this.codeAnalyzer, this.userRules, this.allDefaultRules, this.relevantEngines).toYaml();
return new PlainYamlFormatter(this.codeAnalyzer, this.userRules, this.allDefaultRules, this.relevantEngines, this.includeUnmodifiedRules).toYaml();
} else {
throw new Error(`Unsupported`)
}
Expand All @@ -58,13 +61,15 @@ abstract class YamlFormatter {
private readonly userRules: RuleSelection;
private readonly allDefaultRules: RuleSelection;
private readonly relevantEngines: Set<string>;
private readonly includeUnmodifiedRules: boolean;

protected constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>) {
protected constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>, includeUnmodifiedRules: boolean) {
this.config = codeAnalyzer.getConfig();
this.codeAnalyzer = codeAnalyzer;
this.userRules = userRules;
this.allDefaultRules = allDefaultRules;
this.relevantEngines = relevantEngines;
this.includeUnmodifiedRules = includeUnmodifiedRules;
}

protected abstract toYamlComment(commentText: string): string
Expand All @@ -85,16 +90,13 @@ abstract class YamlFormatter {
}

private toYamlFieldWithFieldDescription(fieldName: string, resolvedValue: unknown, fieldDescription: ConfigFieldDescription): string {
const resolvedValueJson: string = JSON.stringify(resolvedValue);
const defaultValueJson: string = JSON.stringify(fieldDescription.defaultValue);

let yamlField: string;
if (!fieldDescription.wasSuppliedByUser && resolvedValueJson !== defaultValueJson) {
if (!fieldDescription.wasSuppliedByUser && !isSame(resolvedValue, fieldDescription.defaultValue)) {
// Whenever the user did not supply the value themselves but the resolved value is different from the
// default value, this means the value was not a "fixed" value but a value "calculated" at runtime.
// Since "calculated" values often depend on the specific environment, we do not want to actually hard code
// this value into the config since checking in the config to CI/CD system may create a different value.
const commentText: string = getMessage(BundleName.ConfigModel, 'template.last-calculated-as', [resolvedValueJson]);
const commentText: string = getMessage(BundleName.ConfigModel, 'template.last-calculated-as', [JSON.stringify(resolvedValue)]);
yamlField = this.toYamlUncheckedFieldWithInlineComment(fieldName, fieldDescription.defaultValue, commentText);
} else {
yamlField = this.toYamlField(fieldName, resolvedValue, fieldDescription.defaultValue);
Expand All @@ -104,14 +106,10 @@ abstract class YamlFormatter {
}

private toYamlField(fieldName: string, resolvedValue: unknown, defaultValue: unknown): string {
const resolvedValueJson: string = JSON.stringify(resolvedValue);
const defaultValueJson: string = JSON.stringify(defaultValue);

if (resolvedValueJson === defaultValueJson) {
if (isSame(resolvedValue, defaultValue)) {
return this.toYamlUncheckedField(fieldName, resolvedValue);
}

const commentText: string = getMessage(BundleName.ConfigModel, 'template.modified-from', [defaultValueJson]);
const commentText: string = getMessage(BundleName.ConfigModel, 'template.modified-from', [JSON.stringify(defaultValue)]);
resolvedValue = replaceAbsolutePathsWithRelativePathsWherePossible(resolvedValue, this.config.getConfigRoot() + path.sep);
return this.toYamlUncheckedFieldWithInlineComment(fieldName, resolvedValue, commentText);
}
Expand Down Expand Up @@ -155,13 +153,23 @@ abstract class YamlFormatter {
private toYamlRuleOverridesForEngine(engineName: string): string {
const engineConfigHeader: string = getMessage(BundleName.ConfigModel, 'template.rule-overrides-section',
[engineName.toUpperCase()]);
let yamlCode: string = this.toYamlSectionHeadingComment(engineConfigHeader) + '\n';
yamlCode += `${engineName}:\n`;
const ruleOverrideYamlStrings: string[] = [];
for (const userRule of this.userRules.getRulesFor(engineName)) {
const defaultRule: Rule|null = this.getDefaultRuleFor(engineName, userRule.getName());
yamlCode += indent(this.toYamlRuleOverridesForRule(userRule, defaultRule), 2) + '\n';
const ruleOverrideYaml: string = this.toYamlRuleOverridesForRule(userRule, defaultRule);
if (ruleOverrideYaml) {
ruleOverrideYamlStrings.push(indent(ruleOverrideYaml, 2));
}
}
return yamlCode.trimEnd();

let yamlCode: string = this.toYamlSectionHeadingComment(engineConfigHeader) + '\n';
if (ruleOverrideYamlStrings.length > 0) {
yamlCode += `${engineName}:\n` + ruleOverrideYamlStrings.join('\n');
} else {
const commentText: string = getMessage(BundleName.ConfigModel, 'template.yaml.no-rule-overrides');
yamlCode += `${engineName}: {} ${this.toYamlComment(commentText)}`;
}
return yamlCode;
}

private getDefaultRuleFor(engineName: string, ruleName: string): Rule|null {
Expand All @@ -175,15 +183,23 @@ abstract class YamlFormatter {

private toYamlRuleOverridesForRule(userRule: Rule, defaultRule: Rule|null): string {
const userSeverity: SeverityLevel = userRule.getSeverityLevel();
const defaultSeverity: SeverityLevel = defaultRule !== null ? defaultRule.getSeverityLevel() : userSeverity;
const userTags: string[] = userRule.getTags();
return `"${userRule.getName()}":\n` +
indent(this.toYamlField('severity', userSeverity, defaultRule !== null ? defaultRule.getSeverityLevel() : userSeverity), 2) + '\n' +
indent(this.toYamlField('tags', userTags, defaultRule !== null ? defaultRule.getTags() : userTags), 2);
const defaultTags: string[] = defaultRule !== null ? defaultRule.getTags() : userTags;

let yamlCode: string = '';
if (this.includeUnmodifiedRules || !isSame(userSeverity, defaultSeverity)) {
yamlCode += indent(this.toYamlField('severity', userSeverity, defaultSeverity), 2) + '\n';
}
if (this.includeUnmodifiedRules || !isSame(userTags, defaultTags)) {
yamlCode += indent(this.toYamlField('tags', userTags, defaultTags), 2);
}
return yamlCode.length === 0 ? '' : `"${userRule.getName()}":\n${yamlCode.trimEnd()}`;
}

private toYamlEngineOverrides(): string {
if (this.relevantEngines.size === 0) {
const commentText: string = getMessage(BundleName.ConfigModel, 'template.yaml.no-engines-selected');
const commentText: string = getMessage(BundleName.ConfigModel, 'template.yaml.no-rules-selected');
return `engines: {} ${this.toYamlComment(commentText)}`;
}

Expand Down Expand Up @@ -221,8 +237,8 @@ abstract class YamlFormatter {
}

class PlainYamlFormatter extends YamlFormatter {
constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>) {
super(codeAnalyzer, userRules, allDefaultRules, relevantEngines);
constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>, includeUnmodifiedRules: boolean) {
super(codeAnalyzer, userRules, allDefaultRules, relevantEngines, includeUnmodifiedRules);
}

protected toYamlComment(commentText: string): string {
Expand All @@ -231,8 +247,8 @@ class PlainYamlFormatter extends YamlFormatter {
}

class StyledYamlFormatter extends YamlFormatter {
constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>) {
super(codeAnalyzer, userRules, allDefaultRules, relevantEngines);
constructor(codeAnalyzer: CodeAnalyzer, userRules: RuleSelection, allDefaultRules: RuleSelection, relevantEngines: Set<string>, includeUnmodifiedRules: boolean) {
super(codeAnalyzer, userRules, allDefaultRules, relevantEngines, includeUnmodifiedRules);
}

protected toYamlComment(commentText: string): string {
Expand Down Expand Up @@ -262,3 +278,7 @@ function replaceAbsolutePathsWithRelativePathsWherePossible(value: unknown, pare
// Return the value unchanged if it's a number, boolean, or null
return value;
}

function isSame(resolvedValue: unknown, defaultValue: unknown): boolean {
return JSON.stringify(resolvedValue) === JSON.stringify(defaultValue);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
rules:

# ======================================================================
# STUBENGINE1 ENGINE RULE OVERRIDES
# ======================================================================
StubEngine1: {} # Remove this empty object {} when you are ready to specify your first rule override

# ======================================================================
# STUBENGINE3 ENGINE RULE OVERRIDES
# ======================================================================
StubEngine3: {} # Remove this empty object {} when you are ready to specify your first rule override
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# ======================================================================
# STUBENGINE1 ENGINE RULE OVERRIDES
# ======================================================================
StubEngine1:
"Stub1Rule1":
tags: # Modified from: ["Recommended","CodeStyle"]
- Recommended
- CodeStyle
- Beep
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"Stub1Rule2":
severity: 2 # Modified from: 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"Stub1Rule3":
severity: 3 # Modified from: 4
tags: # Modified from: ["BestPractices"]
- CodeStyle
- BestPractices
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"Stub1Rule3":
severity: 3 # Modified from: 4
tags: # Modified from: ["BestPractices"]
- CodeStyle
- BestPractices
"Stub1Rule4":
severity: 2
tags:
- CodeStyle
Loading