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
23 changes: 21 additions & 2 deletions packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,29 @@ 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) {
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) {
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});
Expand Down
7 changes: 5 additions & 2 deletions packages/code-analyzer-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const FIELDS = {
ENGINES: 'engines',
SEVERITY: 'severity',
TAGS: 'tags',
DISABLED: 'disabled',
DISABLE_ENGINE: 'disable_engine',
IGNORES: 'ignores',
FILES: 'files'
Expand All @@ -37,6 +38,7 @@ export type RuleOverrides = Record<string, RuleOverride>;
export type RuleOverride = {
severity?: SeverityLevel
tags?: string[]
disabled?: boolean
}

/**
Expand Down Expand Up @@ -331,11 +333,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)
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/code-analyzer-core/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'.`,

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'.`,

Expand Down
49 changes: 48 additions & 1 deletion packages/code-analyzer-core/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(
Expand Down
189 changes: 189 additions & 0 deletions packages/code-analyzer-core/test/rule-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,195 @@ 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 rules are excluded, a single info log message is emitted with all disabled rules', 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 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('disabled')
);
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 () => {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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"]