Skip to content
Open
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
56 changes: 55 additions & 1 deletion src/lib/models/ConfigModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ConfigFieldDescription,
EngineConfig,
Rule,
RuleOverride,
RuleSelection,
SeverityLevel
} from '@salesforce/code-analyzer-core';
Expand Down Expand Up @@ -127,6 +128,9 @@ abstract class YamlFormatter {
this.toYamlFieldWithFieldDescription('log_level', this.config.getLogLevel(),
topLevelDescription.fieldDescriptions.log_level) + '\n' +
'\n' +
this.toYamlFieldWithFieldDescription('ignores', this.config.getIgnores(),
topLevelDescription.fieldDescriptions.ignores) + '\n' +
'\n' +
this.toYamlComment(topLevelDescription.fieldDescriptions.rules.descriptionText) + '\n' +
this.toYamlRuleOverrides() + '\n' +
'\n' +
Expand Down Expand Up @@ -154,12 +158,31 @@ abstract class YamlFormatter {
const engineConfigHeader: string = getMessage(BundleName.ConfigModel, 'template.rule-overrides-section',
[engineName.toUpperCase()]);
const ruleOverrideYamlStrings: string[] = [];
const processedRuleNames: Set<string> = new Set();

// First, process rules from the user selection (enabled rules)
for (const userRule of this.userRules.getRulesFor(engineName)) {
const defaultRule: Rule|null = this.getDefaultRuleFor(engineName, userRule.getName());
const ruleOverrideYaml: string = this.toYamlRuleOverridesForRule(userRule, defaultRule);
if (ruleOverrideYaml) {
ruleOverrideYamlStrings.push(indent(ruleOverrideYaml, 2));
}
processedRuleNames.add(userRule.getName());
}

// Second, process disabled rules from the user config that weren't in the selection
const userRuleOverrides = this.config.getRuleOverridesFor(engineName);
for (const ruleName of Object.keys(userRuleOverrides)) {
if (!processedRuleNames.has(ruleName)) {
const ruleOverride = userRuleOverrides[ruleName];
if (ruleOverride.disabled === true) {
// Create YAML for disabled rule
const ruleOverrideYaml: string = this.toYamlDisabledRule(engineName, ruleName, ruleOverride);
if (ruleOverrideYaml) {
ruleOverrideYamlStrings.push(indent(ruleOverrideYaml, 2));
}
}
}
}

let yamlCode: string = this.toYamlSectionHeadingComment(engineConfigHeader) + '\n';
Expand Down Expand Up @@ -192,11 +215,42 @@ abstract class YamlFormatter {
yamlCode += indent(this.toYamlField('severity', userSeverity, defaultSeverity), 2) + '\n';
}
if (this.includeUnmodifiedRules || !isSame(userTags, defaultTags)) {
yamlCode += indent(this.toYamlField('tags', userTags, defaultTags), 2);
yamlCode += indent(this.toYamlField('tags', userTags, defaultTags), 2) + '\n';
}

// Get the disabled property from the user's config
const userRuleOverride = this.config.getRuleOverrideFor(userRule.getEngineName(), userRule.getName());
const userDisabled: boolean | undefined = userRuleOverride.disabled;
const defaultDisabled: boolean = false; // Rules are enabled by default

// Include disabled property if explicitly set by user (preserve user customizations)
if (userDisabled !== undefined) {
yamlCode += indent(this.toYamlField('disabled', userDisabled, defaultDisabled), 2);
}

return yamlCode.length === 0 ? '' : `"${userRule.getName()}":\n${yamlCode.trimEnd()}`;
}

private toYamlDisabledRule(engineName: string, ruleName: string, ruleOverride: RuleOverride): string {
let yamlCode: string = '';

// Include severity if user specified it
if (ruleOverride.severity !== undefined) {
yamlCode += indent(this.toYamlUncheckedField('severity', ruleOverride.severity), 2) + '\n';
}

// Include tags if user specified them
if (ruleOverride.tags !== undefined) {
yamlCode += indent(this.toYamlUncheckedField('tags', ruleOverride.tags), 2) + '\n';
}

// Always include disabled property with comparison comment
const defaultDisabled: boolean = false;
yamlCode += indent(this.toYamlField('disabled', true, defaultDisabled), 2);

return yamlCode.length === 0 ? '' : `"${ruleName}":\n${yamlCode.trimEnd()}`;
}

private toYamlEngineOverrides(): string {
if (this.relevantEngines.size === 0) {
const commentText: string = getMessage(BundleName.ConfigModel, 'template.yaml.no-rules-selected');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Config file for testing disabled rules and ignores preservation
config_root: __DUMMY_CONFIG_ROOT__
log_folder: __DUMMY_LOG_FOLDER__

ignores:
files:
- "**/node_modules/**"
- "**/*.test.js"

engines:
StubEngine1:
Property1: "foo"

rules:
StubEngine1:
Stub1Rule1:
severity: "high"
disabled: false
Stub1Rule2:
disabled: true
Stub1Rule3:
severity: "high"
tags: ["CodeStyle"]
disabled: true
95 changes: 95 additions & 0 deletions test/lib/actions/ConfigAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,88 @@ describe('ConfigAction tests', () => {
expect(output).toContain(goldFileContents);
});
});

describe('When there IS an existing config with disabled rules and ignores...', () => {
let configFactory: ConfigFactoryWithDisabledRulesAndIgnores;

beforeEach(() => {
configFactory = new ConfigFactoryWithDisabledRulesAndIgnores();
spyDisplay = new SpyDisplay();
dependencies = {
logEventListeners: [new LogEventDisplayer(spyDisplay)],
progressEventListeners: [],
viewer: new ConfigStyledYamlViewer(spyDisplay),
configFactory: configFactory,
actionSummaryViewer: new ConfigActionSummaryViewer(spyDisplay),
pluginsFactory: new StubEnginePluginFactory()
};
});

it('Ignores section is preserved in output', async () => {
// ==== TESTED BEHAVIOR ====
const output = await runActionAndGetDisplayedConfig(dependencies, ['all']);

// ==== ASSERTIONS ====
expect(output).toContain('ignores:');
expect(output).toContain('files:');
expect(output).toContain('**/node_modules/**');
expect(output).toContain('**/*.test.js');
});

it('Disabled: true rules are preserved with helpful comment', async () => {
// ==== TESTED BEHAVIOR ====
const output = await runActionAndGetDisplayedConfig(dependencies, ['all']);

// ==== ASSERTIONS ====
expect(output).toContain('"Stub1Rule2"');
expect(output).toContain('disabled: true # Modified from: false');
expect(output).toContain('"Stub1Rule3"');
});

it('Disabled: false is preserved when explicitly set', async () => {
// ==== TESTED BEHAVIOR ====
const output = await runActionAndGetDisplayedConfig(dependencies, ['all']);

// ==== ASSERTIONS ====
expect(output).toContain('"Stub1Rule1"');
expect(output).toContain('disabled: false');
});

it('Disabled rules with other overrides preserve all properties', async () => {
// ==== TESTED BEHAVIOR ====
const output = await runActionAndGetDisplayedConfig(dependencies, ['all']);

// ==== ASSERTIONS ====
// Stub1Rule3 has severity, tags, and disabled overrides
expect(output).toContain('"Stub1Rule3"');
expect(output).toContain('severity: 2'); // "high" = 2
expect(output).toContain('tags:');
expect(output).toContain('- CodeStyle');
expect(output).toContain('disabled: true');
});

it('When including unmodified rules, disabled rules are still preserved', async () => {
// ==== TESTED BEHAVIOR ====
const output = await runActionAndGetDisplayedConfig(dependencies, ['all'], undefined, undefined, undefined, true);

// ==== ASSERTIONS ====
expect(output).toContain('"Stub1Rule2"');
expect(output).toContain('disabled: true # Modified from: false');
expect(output).toContain('"Stub1Rule3"');
expect(output).toContain('disabled: true');
});

it('When including unmodified rules, ignores section is still preserved', async () => {
// ==== TESTED BEHAVIOR ====
const output = await runActionAndGetDisplayedConfig(dependencies, ['all'], undefined, undefined, undefined, true);

// ==== ASSERTIONS ====
expect(output).toContain('ignores:');
expect(output).toContain('files:');
expect(output).toContain('**/node_modules/**');
expect(output).toContain('**/*.test.js');
});
});
});

describe('Target/Workspace resolution', () => {
Expand Down Expand Up @@ -665,6 +747,19 @@ class AlternativeStubCodeAnalyzerConfigFactory implements CodeAnalyzerConfigFact
}
}

class ConfigFactoryWithDisabledRulesAndIgnores implements CodeAnalyzerConfigFactory {
dummyConfigRoot: string = 'null';
dummyLogFolder: string = 'null';

public create(): CodeAnalyzerConfig {
const rawConfigFileContents = fs.readFileSync(path.join(PATH_TO_EXAMPLE_WORKSPACE, 'config-with-disabled-rules-and-ignores.yml'), 'utf-8');
const validatedConfigFileContents = rawConfigFileContents
.replaceAll('__DUMMY_CONFIG_ROOT__', this.dummyConfigRoot)
.replaceAll('__DUMMY_LOG_FOLDER__', this.dummyLogFolder);
return CodeAnalyzerConfig.fromYamlString(validatedConfigFileContents, process.cwd());
}
}

class StubEnginePluginFactory implements EnginePluginsFactory {
public create(): EngineApi.EnginePlugin[] {
return [
Expand Down
Loading