From 0948c38641b9428c2387982fcedfae0c5eb8c441 Mon Sep 17 00:00:00 2001 From: David Schach <636977+dschach@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:29:57 +1100 Subject: [PATCH] Skip tab-indented block comment lines in NoMixedIndentation A line whose leading whitespace is one or more tabs, then exactly one space, then an asterisk is the continuation line of a tab-indented block comment, such as ApexDoc. The single space only aligns the asterisk under the opening slash-asterisk, so the line is no longer reported as mixed indentation. Every other mixed-indentation case is still reported. --- .../code-analyzer-regex-engine/package.json | 2 +- .../src/messages.ts | 2 +- .../code-analyzer-regex-engine/src/plugin.ts | 5 +- .../test/engine.test.ts | 46 +++++++++++++++++++ ...ixedIndentation_BlockCommentLookalikes.cls | 21 +++++++++ ...validIndentation_TabsWithBlockComments.cls | 30 ++++++++++++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_BlockCommentLookalikes.cls create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_TabsWithBlockComments.cls diff --git a/packages/code-analyzer-regex-engine/package.json b/packages/code-analyzer-regex-engine/package.json index ce531bbe..2d6dd6c8 100644 --- a/packages/code-analyzer-regex-engine/package.json +++ b/packages/code-analyzer-regex-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-regex-engine", "description": "Plugin package that adds 'regex' as an engine into Salesforce Code Analyzer", - "version": "0.40.0", + "version": "0.41.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", diff --git a/packages/code-analyzer-regex-engine/src/messages.ts b/packages/code-analyzer-regex-engine/src/messages.ts index df2222d1..f71da99f 100644 --- a/packages/code-analyzer-regex-engine/src/messages.ts +++ b/packages/code-analyzer-regex-engine/src/messages.ts @@ -56,7 +56,7 @@ const MESSAGE_CATALOG : { [key: string]: string } = { `Found trailing whitespace at the end of a line of code.`, MixedIndentationRuleDescription: - `Identifies lines with both spaces and tabs in the leading indentation. Mixed indentation causes unexpected behavior in multiline strings because the indentation algorithm processes whitespace differently.`, + `Identifies lines with both spaces and tabs in the leading indentation. Mixed indentation causes unexpected behavior in multiline strings because the indentation algorithm processes whitespace differently. Lines that start with tabs followed by a single space and an asterisk, such as the continuation lines of a tab-indented block comment, are not reported.`, MixedIndentationRuleMessage: `Leading indentation contains mixed spaces and tabs. To fix this, use either spaces or tabs.`, diff --git a/packages/code-analyzer-regex-engine/src/plugin.ts b/packages/code-analyzer-regex-engine/src/plugin.ts index 19dfce4a..46e4dfa6 100644 --- a/packages/code-analyzer-regex-engine/src/plugin.ts +++ b/packages/code-analyzer-regex-engine/src/plugin.ts @@ -80,7 +80,10 @@ export function createBaseRegexRules(now: Date): RegexRules { tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX] }, NoMixedIndentation: { - regex: (/^(?[ \t]*(\t[ ]+|[ ]+\t)[ \t]*)/gm).toString(), + // The negative lookahead skips lines that start with tabs, a single space, and an asterisk. These are the + // continuation lines of tab-indented block comments (like ApexDoc), where the single space aligns the + // asterisk under the asterisk of the opening "/*" or "/**", so they are not mixed indentation. + regex: (/^(?!\t+ \*)(?[ \t]*(\t[ ]+|[ ]+\t)[ \t]*)/gm).toString(), file_extensions: ['.cls', '.trigger'], description: getMessage('MixedIndentationRuleDescription'), violation_message: getMessage('MixedIndentationRuleMessage'), diff --git a/packages/code-analyzer-regex-engine/test/engine.test.ts b/packages/code-analyzer-regex-engine/test/engine.test.ts index 7fa244f2..ee8d6e46 100644 --- a/packages/code-analyzer-regex-engine/test/engine.test.ts +++ b/packages/code-analyzer-regex-engine/test/engine.test.ts @@ -1029,8 +1029,54 @@ describe('Tests for runRules', () => { expect(violation.message).toBe(getMessage('MixedIndentationRuleMessage')); } }); + + it("NoMixedIndentation rule should NOT flag block comment continuation lines that are indented with tabs then a single space", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_TabsWithBlockComments.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + // ApexDoc and other block comments indented with tabs have continuation lines of tabs, one space, and an + // asterisk. The single space only aligns the asterisk under the opening "/*", so it is not mixed indentation. + expect(runResults.violations).toHaveLength(0); + }); + + it("NoMixedIndentation rule should still flag lines that resemble block comment continuation lines but are mixed indentation", async () => { + const file: string = path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_BlockCommentLookalikes.cls"); + const runOptions: RunOptions = createRunOptions(new Workspace('id', [file])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + const expectedViolations: Violation[] = [ + // 1. Tabs, then two spaces, then an asterisk + createExpectedMixedIndentationViolation(file, 5, 4), + // 2. A space, then a tab, then a space, then an asterisk + createExpectedMixedIndentationViolation(file, 9, 4), + // 3. Tabs, then a single space, then a character that is not an asterisk + createExpectedMixedIndentationViolation(file, 13, 3), + // 4. Tabs, then two spaces, then text + createExpectedMixedIndentationViolation(file, 17, 5) + ]; + expect(runResults.violations).toHaveLength(expectedViolations.length); + expect(runResults.violations).toEqual(expectedViolations); + }); }); +function createExpectedMixedIndentationViolation(file: string, line: number, endColumn: number): Violation { + return { + ruleName: "NoMixedIndentation", + message: getMessage('MixedIndentationRuleMessage'), + primaryLocationIndex: 0, + codeLocations: [ + { + file: file, + startLine: line, + startColumn: 1, + endLine: line, + endColumn: endColumn + } + ] + }; +} + describe('Tests for regex_ignore', () => { it('regex_ignore should exclude matches that match the negative pattern', async () => { const customRulesWithNegativePattern: RegexRules = { diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_BlockCommentLookalikes.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_BlockCommentLookalikes.cls new file mode 100644 index 00000000..bdf76a75 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_BlockCommentLookalikes.cls @@ -0,0 +1,21 @@ +public class BlockCommentLookalikes { + // This file SHOULD trigger violations. Each case is close to, but is not, a tab-indented block comment line. + + /** + * 1. Tabs, then two spaces, then an asterisk + */ + + /** + * 2. A space, then a tab, then a space, then an asterisk + */ + + /** + 3. Tabs, then a single space, then a character that is not an asterisk + */ + + /* + 4. Tabs, then two spaces, then text + */ + + public void method() {} +} diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_TabsWithBlockComments.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_TabsWithBlockComments.cls new file mode 100644 index 00000000..0d88bba7 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_TabsWithBlockComments.cls @@ -0,0 +1,30 @@ +/** + * The ApexDoc comment on the class is not indented, so this file starts with the usual space-asterisk lines. + */ +public class TabsWithBlockComments { + /** + * @description Every continuation line of this ApexDoc comment is a tab, a single space, and an asterisk. + * @param accountId The Id of the Account to query + * @return The Account with the given Id + */ + public static Account getAccount(Id accountId) { + /* + * A plain block comment formatted the same way, two tabs deep. + */ + return [SELECT Id FROM Account WHERE Id = :accountId]; + } + + /** + * An ApexDoc comment that contains a block quote and a code block. + * + * > Each line of this block quote starts with a tab, a single space, and an asterisk. + * > So it is a block comment continuation line, not mixed indentation. + * + * ``` + * Account a = getAccount(someId); + * ``` + */ + public static void example() { + System.debug('ok'); + } +}