From ed019c99b34ef31d01cdf1a275dadb930f5479d4 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Thu, 12 Feb 2026 11:39:25 +0530 Subject: [PATCH 1/4] Adding changes to disable rules from being selected --- .../code-analyzer-core/src/code-analyzer.ts | 14 +- packages/code-analyzer-core/src/config.ts | 6 +- packages/code-analyzer-core/src/messages.ts | 7 + .../code-analyzer-core/test/config.test.ts | 49 +++++- .../test/rule-selection.test.ts | 157 ++++++++++++++++++ 5 files changed, 228 insertions(+), 5 deletions(-) diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index 04e53467..d29005cd 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -301,9 +301,19 @@ export class CodeAnalyzer { const ruleSelection: RuleSelectionImpl = new RuleSelectionImpl(); for (const rule of allRules) { - if (selectorObjects.some(o => rule.matchesRuleSelector(o))) { - ruleSelection.addRule(rule); + // Skip rules that don't match any selector + if (!selectorObjects.some(o => rule.matchesRuleSelector(o))) { + continue; } + + // Skip rules that are disabled in the config + const ruleOverride = this.config.getRuleOverrideFor(rule.getEngineName(), rule.getName()); + if (ruleOverride.disabled === true) { + this.emitLogEvent(LogLevel.Info, getMessage('RuleDisabledInConfig', rule.getName(), rule.getEngineName())); + continue; + } + + ruleSelection.addRule(rule); } this.emitEvent({type: EventType.RuleSelectionProgressEvent, timestamp: this.clock.now(), percentComplete: 100}); diff --git a/packages/code-analyzer-core/src/config.ts b/packages/code-analyzer-core/src/config.ts index 627670f1..e7aa3f44 100644 --- a/packages/code-analyzer-core/src/config.ts +++ b/packages/code-analyzer-core/src/config.ts @@ -20,6 +20,7 @@ export const FIELDS = { ENGINES: 'engines', SEVERITY: 'severity', TAGS: 'tags', + DISABLED: 'disabled', DISABLE_ENGINE: 'disable_engine', IGNORES: 'ignores', FILES: 'files' @@ -331,11 +332,12 @@ function extractRuleOverridesFrom(engineRuleOverridesExtractor: engApi.ConfigVal } function extractRuleOverrideFrom(ruleOverrideExtractor: engApi.ConfigValueExtractor): RuleOverride { - ruleOverrideExtractor.validateContainsOnlySpecifiedKeys([FIELDS.SEVERITY, FIELDS.TAGS]); + ruleOverrideExtractor.validateContainsOnlySpecifiedKeys([FIELDS.SEVERITY, FIELDS.TAGS, FIELDS.DISABLED]); const engSeverity: engApi.SeverityLevel | undefined = ruleOverrideExtractor.extractSeverityLevel(FIELDS.SEVERITY); return { tags: ruleOverrideExtractor.extractArray(FIELDS.TAGS, engApi.ValueValidator.validateString), - severity: engSeverity === undefined ? undefined : engSeverity as SeverityLevel + severity: engSeverity === undefined ? undefined : engSeverity as SeverityLevel, + disabled: ruleOverrideExtractor.extractBoolean(FIELDS.DISABLED) } } diff --git a/packages/code-analyzer-core/src/messages.ts b/packages/code-analyzer-core/src/messages.ts index e63e2603..c758b515 100644 --- a/packages/code-analyzer-core/src/messages.ts +++ b/packages/code-analyzer-core/src/messages.ts @@ -33,12 +33,16 @@ const MESSAGE_CATALOG : MessageCatalog = { ` 'severity' - [Optional] The severity level value that you want to use to override the default severity level for the rule\n` + ` Possible values: 1 or 'Critical', 2 or 'High', 3 or 'Moderate', 4 or 'Low', 5 or 'Info'\n` + ` 'tags' - [Optional] The string array of tag values that you want to use to override the default tags for the rule\n` + + ` 'disabled' - [Optional] Boolean value to disable the rule for all files. When set to true, the rule will not run during analysis\n` + `---- [Example usage]: ---------------------\n` + `rules:\n` + ` eslint:\n` + ` sort-vars:\n` + ` severity: "Info"\n` + ` tags: ["Recommended", "Suggestion"]\n` + + ` regex:\n` + + ` NoTrailingWhiteSpace:\n` + + ` disabled: true\n` + `-------------------------------------------`, ConfigFieldDescription_engines: @@ -143,6 +147,9 @@ const MESSAGE_CATALOG : MessageCatalog = { RulePropertyOverridden: `The %s value of rule '%s' of engine '%s' was overridden according to the specified configuration. The old value '%s' was replaced with the new value '%s'.`, + RuleDisabledInConfig: + `Rule '%s' from engine '%s' was disabled according to the specified configuration and will not be included in the rule selection.`, + ConfigPathValueMustBeAbsolute: `The '%s' configuration value must be provided as an absolute path location. Update the value '%s' to instead be '%s'.`, diff --git a/packages/code-analyzer-core/test/config.test.ts b/packages/code-analyzer-core/test/config.test.ts index 7f43eac2..07ce276d 100644 --- a/packages/code-analyzer-core/test/config.test.ts +++ b/packages/code-analyzer-core/test/config.test.ts @@ -209,7 +209,7 @@ describe("Tests for creating and accessing configuration values", () => { it("When rules.someEngine.someRule contains an unknown key then we throw an error", () => { expect(() => CodeAnalyzerConfig.fromObject({rules: {someEngine: {someRule: {oops: 3, tags: []}}}})).toThrow( getMessageFromCatalog(SHARED_MESSAGE_CATALOG,'ConfigObjectContainsInvalidKey','rules.someEngine.someRule', 'oops', - '["severity","tags"]')); + '["disabled","severity","tags"]')); }); it("When the severity of a rule not a valid value then we throw an error", () => { @@ -246,6 +246,53 @@ describe("Tests for creating and accessing configuration values", () => { expect(conf.getRuleOverridesFor('someEngine')).toEqual(someRuleOverrides); }); + it("When disabled is set to true for a rule, then we correctly store the disabled property", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({rules: {someEngine: { + someRule1: {disabled: true}, + someRule2: {disabled: false}, + someRule3: {severity: 3} // No disabled property + }}}); + expect(conf.getRuleOverrideFor('someEngine', 'someRule1')).toEqual({disabled: true}); + expect(conf.getRuleOverrideFor('someEngine', 'someRule2')).toEqual({disabled: false}); + expect(conf.getRuleOverrideFor('someEngine', 'someRule3')).toEqual({severity: SeverityLevel.Moderate}); + }); + + it("When disabled is combined with other rule properties, then all properties are correctly stored", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({rules: {someEngine: { + someRule: { + disabled: true, + severity: 2, + tags: ['Security', 'Custom'] + } + }}}); + expect(conf.getRuleOverrideFor('someEngine', 'someRule')).toEqual({ + disabled: true, + severity: SeverityLevel.High, + tags: ['Security', 'Custom'] + }); + }); + + it("When disabled is not a boolean value, then we throw an error", () => { + expect(() => CodeAnalyzerConfig.fromObject({rules: {someEngine: { + someRule: {disabled: 'yes'} + }}})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG,'ConfigValueMustBeOfType','rules.someEngine.someRule.disabled', 'boolean', 'string')); + + expect(() => CodeAnalyzerConfig.fromObject({rules: {someEngine: { + someRule: {disabled: 1} + }}})).toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG,'ConfigValueMustBeOfType','rules.someEngine.someRule.disabled', 'boolean', 'number')); + }); + + it("When loading config from yaml file with disabled rules, then disabled property is parsed correctly", () => { + const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromFile(path.join(TEST_DATA_DIR, 'sample-config-with-disabled-rule.yaml')); + expect(conf.getRuleOverrideFor('stubEngine1', 'stub1RuleC')).toEqual({disabled: true}); + expect(conf.getRuleOverrideFor('stubEngine2', 'stub2RuleA')).toEqual({ + disabled: true, + tags: ['Security', 'SomeNewTag'] + }); + }); + it("When log_folder does not exist, then throw an error", () => { const nonExistingFolder: string = path.resolve(__dirname, "doesNotExist"); expect(() => CodeAnalyzerConfig.fromObject({log_folder: nonExistingFolder})).toThrow( diff --git a/packages/code-analyzer-core/test/rule-selection.test.ts b/packages/code-analyzer-core/test/rule-selection.test.ts index 3882450c..a41f6101 100644 --- a/packages/code-analyzer-core/test/rule-selection.test.ts +++ b/packages/code-analyzer-core/test/rule-selection.test.ts @@ -403,6 +403,163 @@ describe('Tests for selecting rules', () => { expect(ruleNamesFor(selection, 'stubEngine2')).toEqual([]); }); + it('When config contains disabled:true for a rule, then that rule is not selected', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { disabled: true } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['Recommended']); + + // stub1RuleB should be excluded even though it's Recommended + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleC']); + expect(ruleNamesFor(selection, 'stubEngine2')).toEqual(['stub2RuleA', 'stub2RuleC']); + }); + + it('When config contains disabled:false for a rule, then that rule is still selected normally', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { disabled: false } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['Recommended']); + + // stub1RuleB should be included since disabled is explicitly false + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleB', 'stub1RuleC']); + expect(ruleNamesFor(selection, 'stubEngine2')).toEqual(['stub2RuleA', 'stub2RuleC']); + }); + + it('When multiple rules are disabled via config, then all disabled rules are excluded from selection', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleA: { disabled: true }, + stub1RuleB: { disabled: true }, + stub1RuleC: { disabled: true } + }, + stubEngine2: { + stub2RuleA: { disabled: true } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['Recommended']); + + // All disabled rules should be excluded + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual([]); + expect(ruleNamesFor(selection, 'stubEngine2')).toEqual(['stub2RuleC']); + expect(ruleNamesFor(selection, 'stubEngine3')).toEqual(['stub3RuleA']); + }); + + it('When disabled rule is explicitly selected by name, it is still excluded', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { disabled: true } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['stub1RuleB']); + + // Even though we explicitly selected stub1RuleB, it should be excluded because it's disabled + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual([]); + }); + + it('When disabled rule is selected via engine name, it is still excluded', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { disabled: true }, + stub1RuleD: { disabled: true } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['stubEngine1']); + + // When selecting all rules from stubEngine1, disabled rules should be excluded + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleC', 'stub1RuleE']); + }); + + it('When disabled rule is selected via all selector, it is still excluded', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { disabled: true } + }, + stubEngine2: { + stub2RuleA: { disabled: true } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['all']); + + // When selecting all rules, disabled rules should be excluded + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleC', 'stub1RuleD', 'stub1RuleE']); + expect(ruleNamesFor(selection, 'stubEngine2')).toEqual(['stub2RuleB', 'stub2RuleC']); + }); + + it('When disabled is combined with other property overrides, the rule is still excluded', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { + disabled: true, + severity: 1, // Changed to Critical + tags: ['NewTag'] + } + } + } + })); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['Recommended']); + + // stub1RuleB should be excluded despite having other property overrides + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleC']); + }); + + it('When disabled rule is excluded, an info log message is emitted', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleB: { disabled: true } + } + } + })); + + const logEvents: LogEvent[] = []; + codeAnalyzer.onEvent(EventType.LogEvent, (event: LogEvent) => logEvents.push(event)); + + await codeAnalyzer.selectRules(['Recommended']); + + // Should have an info log message about the disabled rule + const disabledRuleLogEvents = logEvents.filter(e => + e.logLevel === LogLevel.Info && + e.message.includes('stub1RuleB') && + e.message.includes('disabled') + ); + expect(disabledRuleLogEvents.length).toBeGreaterThan(0); + expect(disabledRuleLogEvents[0].message).toEqual(getMessage('RuleDisabledInConfig', 'stub1RuleB', 'stubEngine1')); + }); + + it('When loading config from yaml file with disabled rules, disabled rules are excluded from selection', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromFile(path.resolve(__dirname, "test-data", "sample-config-with-disabled-rule.yaml"))); + + const selection: RuleSelection = await codeAnalyzer.selectRules(['all']); + + // stub1RuleC and stub2RuleA should be excluded because they're disabled in the config file + expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleB', 'stub1RuleD', 'stub1RuleE']); + expect(ruleNamesFor(selection, 'stubEngine2')).toEqual(['stub2RuleB', 'stub2RuleC']); + }); + it('When an engine fails to return its rules, an error is logged and empty results are returned', async () => { // ====== TEST SETUP ====== codeAnalyzer = createCodeAnalyzer(); From 58eebab8a3cb94414afc0b4e164fbe0511c6a900 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 13 Feb 2026 13:50:35 +0530 Subject: [PATCH 2/4] adding the missed config file --- packages/code-analyzer-core/src/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-analyzer-core/src/config.ts b/packages/code-analyzer-core/src/config.ts index e7aa3f44..996ca479 100644 --- a/packages/code-analyzer-core/src/config.ts +++ b/packages/code-analyzer-core/src/config.ts @@ -38,6 +38,7 @@ export type RuleOverrides = Record; export type RuleOverride = { severity?: SeverityLevel tags?: string[] + disabled?: boolean } /** From 10ecd0e39dff945eeccf1d26c3a129a042e8d452 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Fri, 13 Feb 2026 13:54:55 +0530 Subject: [PATCH 3/4] Adding config file --- .../sample-config-with-disabled-rule.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/code-analyzer-core/test/test-data/sample-config-with-disabled-rule.yaml diff --git a/packages/code-analyzer-core/test/test-data/sample-config-with-disabled-rule.yaml b/packages/code-analyzer-core/test/test-data/sample-config-with-disabled-rule.yaml new file mode 100644 index 00000000..2d5e9113 --- /dev/null +++ b/packages/code-analyzer-core/test/test-data/sample-config-with-disabled-rule.yaml @@ -0,0 +1,16 @@ +log_folder: sampleLogFolder + +rules: + stubEngine1: + stub1RuleB: + severity: 1 + stub1RuleC: + disabled: true + stub1RuleD: + severity: "Info" + tags: ["Recommended", "CodeStyle"] + + stubEngine2: + stub2RuleA: + disabled: true + tags: ["Security", "SomeNewTag"] From 619228d79634ca5ce48de4c68ab26011d6e47c25 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Tue, 17 Feb 2026 09:25:40 +0530 Subject: [PATCH 4/4] Creating combined log for disabled rules --- .../code-analyzer-core/src/code-analyzer.ts | 11 ++++- packages/code-analyzer-core/src/messages.ts | 4 +- .../test/rule-selection.test.ts | 42 ++++++++++++++++--- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index d29005cd..3120438e 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -300,6 +300,8 @@ export class CodeAnalyzer { const allRules: RuleImpl[] = await this.getAllRules(selectOptions?.workspace); const ruleSelection: RuleSelectionImpl = new RuleSelectionImpl(); + const disabledRules: {ruleName: string, engineName: string}[] = []; + for (const rule of allRules) { // Skip rules that don't match any selector if (!selectorObjects.some(o => rule.matchesRuleSelector(o))) { @@ -309,13 +311,20 @@ export class CodeAnalyzer { // Skip rules that are disabled in the config const ruleOverride = this.config.getRuleOverrideFor(rule.getEngineName(), rule.getName()); if (ruleOverride.disabled === true) { - this.emitLogEvent(LogLevel.Info, getMessage('RuleDisabledInConfig', rule.getName(), rule.getEngineName())); + disabledRules.push({ruleName: rule.getName(), engineName: rule.getEngineName()}); continue; } ruleSelection.addRule(rule); } + // Log all disabled rules at once + if (disabledRules.length > 0) { + this.emitLogEvent(LogLevel.Info, getMessage('RulesDisabledInConfig', + disabledRules.length, + disabledRules.map(r => `${r.engineName}:${r.ruleName}`).join(', '))); + } + this.emitEvent({type: EventType.RuleSelectionProgressEvent, timestamp: this.clock.now(), percentComplete: 100}); return ruleSelection; } diff --git a/packages/code-analyzer-core/src/messages.ts b/packages/code-analyzer-core/src/messages.ts index c758b515..dfff6b2a 100644 --- a/packages/code-analyzer-core/src/messages.ts +++ b/packages/code-analyzer-core/src/messages.ts @@ -147,8 +147,8 @@ const MESSAGE_CATALOG : MessageCatalog = { RulePropertyOverridden: `The %s value of rule '%s' of engine '%s' was overridden according to the specified configuration. The old value '%s' was replaced with the new value '%s'.`, - RuleDisabledInConfig: - `Rule '%s' from engine '%s' was disabled according to the specified configuration and will not be included in the rule selection.`, + RulesDisabledInConfig: + `%d rule(s) were disabled according to the specified configuration and will not be included in the rule selection: %s`, ConfigPathValueMustBeAbsolute: `The '%s' configuration value must be provided as an absolute path location. Update the value '%s' to instead be '%s'.`, diff --git a/packages/code-analyzer-core/test/rule-selection.test.ts b/packages/code-analyzer-core/test/rule-selection.test.ts index a41f6101..37bdefbd 100644 --- a/packages/code-analyzer-core/test/rule-selection.test.ts +++ b/packages/code-analyzer-core/test/rule-selection.test.ts @@ -526,7 +526,7 @@ describe('Tests for selecting rules', () => { expect(ruleNamesFor(selection, 'stubEngine1')).toEqual(['stub1RuleA', 'stub1RuleC']); }); - it('When disabled rule is excluded, an info log message is emitted', async () => { + it('When disabled rules are excluded, a single info log message is emitted with all disabled rules', async () => { await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ rules: { stubEngine1: { @@ -540,14 +540,46 @@ describe('Tests for selecting rules', () => { await codeAnalyzer.selectRules(['Recommended']); - // Should have an info log message about the disabled rule + // Should have a single info log message about all disabled rules + const disabledRuleLogEvents = logEvents.filter(e => + e.logLevel === LogLevel.Info && + e.message.includes('disabled') && + e.message.includes('stubEngine1:stub1RuleB') + ); + expect(disabledRuleLogEvents.length).toBe(1); + expect(disabledRuleLogEvents[0].message).toEqual(getMessage('RulesDisabledInConfig', 1, 'stubEngine1:stub1RuleB')); + }); + + it('When multiple disabled rules are excluded, they are all listed in a single log message', async () => { + await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({ + rules: { + stubEngine1: { + stub1RuleA: { disabled: true }, + stub1RuleB: { disabled: true } + }, + stubEngine2: { + stub2RuleA: { disabled: true } + } + } + })); + + const logEvents: LogEvent[] = []; + codeAnalyzer.onEvent(EventType.LogEvent, (event: LogEvent) => logEvents.push(event)); + + await codeAnalyzer.selectRules(['Recommended']); + + // Should have a single info log message listing all 3 disabled rules const disabledRuleLogEvents = logEvents.filter(e => e.logLevel === LogLevel.Info && - e.message.includes('stub1RuleB') && e.message.includes('disabled') ); - expect(disabledRuleLogEvents.length).toBeGreaterThan(0); - expect(disabledRuleLogEvents[0].message).toEqual(getMessage('RuleDisabledInConfig', 'stub1RuleB', 'stubEngine1')); + expect(disabledRuleLogEvents.length).toBe(1); + + const message = disabledRuleLogEvents[0].message; + expect(message).toContain('3 rule(s)'); + expect(message).toContain('stubEngine1:stub1RuleA'); + expect(message).toContain('stubEngine1:stub1RuleB'); + expect(message).toContain('stubEngine2:stub2RuleA'); }); it('When loading config from yaml file with disabled rules, disabled rules are excluded from selection', async () => {