From e54cb0ada0a6fe830cfdb59bb311f1982fee07fb Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 27 Mar 2026 12:19:46 +0530 Subject: [PATCH 1/2] Adding changes to ensure ignore object and disable rules are preserved as it is --- src/lib/models/ConfigModel.ts | 56 ++++++++++- ...config-with-disabled-rules-and-ignores.yml | 24 +++++ test/lib/actions/ConfigAction.test.ts | 95 +++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/example-workspaces/ConfigAction.test.ts/config-with-disabled-rules-and-ignores.yml diff --git a/src/lib/models/ConfigModel.ts b/src/lib/models/ConfigModel.ts index 3adadc60f..675ea590b 100644 --- a/src/lib/models/ConfigModel.ts +++ b/src/lib/models/ConfigModel.ts @@ -7,6 +7,7 @@ import { ConfigFieldDescription, EngineConfig, Rule, + RuleOverride, RuleSelection, SeverityLevel } from '@salesforce/code-analyzer-core'; @@ -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' + @@ -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 = 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'; @@ -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 (no comparison comment needed - rule is disabled) + if (ruleOverride.severity !== undefined) { + yamlCode += indent(this.toYamlUncheckedField('severity', ruleOverride.severity), 2) + '\n'; + } + + // Include tags if user specified them (no comparison comment needed - rule is disabled) + if (ruleOverride.tags !== undefined) { + yamlCode += indent(this.toYamlUncheckedField('tags', ruleOverride.tags), 2) + '\n'; + } + + // Always include disabled property WITH comparison comment (this is the important one) + 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'); diff --git a/test/fixtures/example-workspaces/ConfigAction.test.ts/config-with-disabled-rules-and-ignores.yml b/test/fixtures/example-workspaces/ConfigAction.test.ts/config-with-disabled-rules-and-ignores.yml new file mode 100644 index 000000000..2a672dff9 --- /dev/null +++ b/test/fixtures/example-workspaces/ConfigAction.test.ts/config-with-disabled-rules-and-ignores.yml @@ -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 diff --git a/test/lib/actions/ConfigAction.test.ts b/test/lib/actions/ConfigAction.test.ts index 6c0327654..b33a7ba3d 100644 --- a/test/lib/actions/ConfigAction.test.ts +++ b/test/lib/actions/ConfigAction.test.ts @@ -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', () => { @@ -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 [ From c59ce1bc7ecd9099e514482892ad8db5facb6c94 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 27 Mar 2026 12:33:32 +0530 Subject: [PATCH 2/2] fixing comment --- src/lib/models/ConfigModel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/models/ConfigModel.ts b/src/lib/models/ConfigModel.ts index 675ea590b..a26c12b4c 100644 --- a/src/lib/models/ConfigModel.ts +++ b/src/lib/models/ConfigModel.ts @@ -234,17 +234,17 @@ abstract class YamlFormatter { private toYamlDisabledRule(engineName: string, ruleName: string, ruleOverride: RuleOverride): string { let yamlCode: string = ''; - // Include severity if user specified it (no comparison comment needed - rule is disabled) + // 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 (no comparison comment needed - rule is disabled) + // 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 (this is the important one) + // Always include disabled property with comparison comment const defaultDisabled: boolean = false; yamlCode += indent(this.toYamlField('disabled', true, defaultDisabled), 2);