From 15aae89b8cb376d48272709d1c99cbc3d443b933 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Thu, 12 Mar 2026 16:17:03 +0530 Subject: [PATCH 1/3] Add NoMixedIndentation rule to regex engine Adds new rule to detect lines where leading indentation contains both spaces and tabs. This is important for Apex multiline strings (v262) where mixed indentation can cause unexpected behavior due to the indentation algorithm treating whitespace differently. Changes: - Added NoMixedIndentation rule in plugin.ts with regex pattern /^(?[ \t]*(\t[ ]+|[ ]+\t)[ \t]*)/gm - Rule applies to .cls and .trigger files only - Severity: Moderate (3) - Tags: Recommended, CodeStyle, Apex - Added rule description and violation message in messages.ts - Added 4 test files covering: * Tab followed by spaces (violation) * Spaces followed by tab (violation) * Only spaces (valid) * Only tabs (valid) - Added unit tests in engine.test.ts - Updated existing tests to account for new rule count - Bumped version to 0.33.1-SNAPSHOT All tests passing (72 tests, 100% coverage on main files) --- .../code-analyzer-regex-engine/package.json | 2 +- .../src/messages.ts | 6 ++ .../code-analyzer-regex-engine/src/plugin.ts | 8 ++ .../test/engine.test.ts | 76 +++++++++++++++---- .../mixedIndentation_SpacesThenTab.cls | 8 ++ .../mixedIndentation_TabThenSpaces.cls | 8 ++ .../validIndentation_OnlySpaces.cls | 8 ++ .../validIndentation_OnlyTabs.cls | 8 ++ 8 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_SpacesThenTab.cls create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_TabThenSpaces.cls create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlySpaces.cls create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlyTabs.cls diff --git a/packages/code-analyzer-regex-engine/package.json b/packages/code-analyzer-regex-engine/package.json index d3d3b273..681e2b73 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.33.0", + "version": "0.33.1-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 3d9ba751..ceeb454a 100644 --- a/packages/code-analyzer-regex-engine/src/messages.ts +++ b/packages/code-analyzer-regex-engine/src/messages.ts @@ -43,6 +43,12 @@ const MESSAGE_CATALOG : { [key: string]: string } = { TrailingWhitespaceRuleMessage: `Found trailing whitespace at the end of a line of code.`, + MixedIndentationRuleDescription: + `Detects lines where leading indentation contains both spaces and tabs. Mixed indentation can cause unexpected behavior in multiline strings (introduced in Apex API v262) due to how the indentation algorithm processes whitespace.`, + + MixedIndentationRuleMessage: + `Found mixed spaces and tabs in leading indentation. Use either spaces or tabs consistently for indentation, not both.`, + AvoidTermsWithImplicitBiasRuleDescription: `"Detects usage of terms that reinforce implicit bias.`, diff --git a/packages/code-analyzer-regex-engine/src/plugin.ts b/packages/code-analyzer-regex-engine/src/plugin.ts index e44c3fef..19dfce4a 100644 --- a/packages/code-analyzer-regex-engine/src/plugin.ts +++ b/packages/code-analyzer-regex-engine/src/plugin.ts @@ -79,6 +79,14 @@ export function createBaseRegexRules(now: Date): RegexRules { severity: SeverityLevel.Info, tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX] }, + NoMixedIndentation: { + regex: (/^(?[ \t]*(\t[ ]+|[ ]+\t)[ \t]*)/gm).toString(), + file_extensions: ['.cls', '.trigger'], + description: getMessage('MixedIndentationRuleDescription'), + violation_message: getMessage('MixedIndentationRuleMessage'), + severity: SeverityLevel.Moderate, + tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX] + }, AvoidTermsWithImplicitBias: { // file_extensions not listed so that it can run on all text files regex: (/\b(((black|white)\s*list\w*)|((black|brown)\s*out\w*)|(slaves?\b))/gi).toString(), description: getMessage('AvoidTermsWithImplicitBiasRuleDescription'), diff --git a/packages/code-analyzer-regex-engine/test/engine.test.ts b/packages/code-analyzer-regex-engine/test/engine.test.ts index b852423d..aa66fcc2 100644 --- a/packages/code-analyzer-regex-engine/test/engine.test.ts +++ b/packages/code-analyzer-regex-engine/test/engine.test.ts @@ -52,6 +52,14 @@ const EXPECTED_NoTrailingWhitespace_RULE_DESCRIPTION: RuleDescription = { resourceUrls: [] }; +const EXPECTED_NoMixedIndentation_RULE_DESCRIPTION: RuleDescription = { + name: "NoMixedIndentation", + severityLevel: SeverityLevel.Moderate, + tags: ["Recommended", "CodeStyle", "Apex"], + description: getMessage('MixedIndentationRuleDescription'), + resourceUrls: [] +}; + const EXPECTED_NoTodos_RULE_DESCRIPTION: RuleDescription = { name: "NoTodos", severityLevel: DEFAULT_SEVERITY_LEVEL, @@ -126,15 +134,16 @@ describe("Tests for RegexEngine's getName and describeRules methods", () => { it('Calling describeRules without workspace, returns all available rules', async () => { const rulesDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions()); - expect(rulesDescriptions).toHaveLength(8); + expect(rulesDescriptions).toHaveLength(9); expect(rulesDescriptions[0]).toMatchObject(EXPECTED_NoTrailingWhitespace_RULE_DESCRIPTION); - expect(rulesDescriptions[1]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION) - expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION) - expect(rulesDescriptions[3]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION) - expect(rulesDescriptions[4]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION) - expect(rulesDescriptions[5]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION); - expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION); - expect(rulesDescriptions[7]).toMatchObject(EXPECTED_NoTalkingAboutFightClub_RULE_DESCRIPTION); + expect(rulesDescriptions[1]).toMatchObject(EXPECTED_NoMixedIndentation_RULE_DESCRIPTION); + expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION); + expect(rulesDescriptions[3]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION); + expect(rulesDescriptions[4]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION); + expect(rulesDescriptions[5]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION); + expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION); + expect(rulesDescriptions[7]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION); + expect(rulesDescriptions[8]).toMatchObject(EXPECTED_NoTalkingAboutFightClub_RULE_DESCRIPTION); }); it("When workspace targeting zero applicable files, then describeRules returns no rules", async () => { @@ -157,14 +166,15 @@ describe("Tests for RegexEngine's getName and describeRules methods", () => { it("When workspace contains files are applicable to all available rules, then describeRules returns all rules", async () => { const rulesDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions( new Workspace('id', [path.resolve(__dirname, 'test-data', 'sampleWorkspace')]))); - expect(rulesDescriptions).toHaveLength(7); + expect(rulesDescriptions).toHaveLength(8); expect(rulesDescriptions[0]).toMatchObject(EXPECTED_NoTrailingWhitespace_RULE_DESCRIPTION); - expect(rulesDescriptions[1]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION); - expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION); - expect(rulesDescriptions[3]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION); - expect(rulesDescriptions[4]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION); - expect(rulesDescriptions[5]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION); - expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION); + expect(rulesDescriptions[1]).toMatchObject(EXPECTED_NoMixedIndentation_RULE_DESCRIPTION); + expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION); + expect(rulesDescriptions[3]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION); + expect(rulesDescriptions[4]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION); + expect(rulesDescriptions[5]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION); + expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION); + expect(rulesDescriptions[7]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION); }); }); @@ -959,6 +969,42 @@ describe('Tests for runRules', () => { expect(combinedRunViolations).toContainEqual(individualRunViolation); } }); + + it("NoMixedIndentation rule should detect tab followed by spaces in indentation", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_TabThenSpaces.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + expect(runResults.violations.length).toBeGreaterThan(0); + expect(runResults.violations[0].ruleName).toBe("NoMixedIndentation"); + expect(runResults.violations[0].message).toBe(getMessage('MixedIndentationRuleMessage')); + }); + + it("NoMixedIndentation rule should detect spaces followed by tab in indentation", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_SpacesThenTab.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + expect(runResults.violations.length).toBeGreaterThan(0); + expect(runResults.violations[0].ruleName).toBe("NoMixedIndentation"); + expect(runResults.violations[0].message).toBe(getMessage('MixedIndentationRuleMessage')); + }); + + it("NoMixedIndentation rule should NOT flag lines with only spaces", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_OnlySpaces.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + expect(runResults.violations).toHaveLength(0); + }); + + it("NoMixedIndentation rule should NOT flag lines with only tabs", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_OnlyTabs.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + expect(runResults.violations).toHaveLength(0); + }); }); describe('Tests for getEngineVersion', () => { diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_SpacesThenTab.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_SpacesThenTab.cls new file mode 100644 index 00000000..9ef3a2b0 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_SpacesThenTab.cls @@ -0,0 +1,8 @@ +public class MixedIndentationExample { + public void testMultilineString() { + // This has mixed indentation: spaces followed by tab + String testMultiLine = ''' + [hello + world]'''; + } +} diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_TabThenSpaces.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_TabThenSpaces.cls new file mode 100644 index 00000000..fd1b70b2 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_TabThenSpaces.cls @@ -0,0 +1,8 @@ +public class MixedIndentationExample { + public void testMultilineString() { + // This has mixed indentation: tab followed by spaces + String testMultiLine = ''' + [hello + world]'''; + } +} diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlySpaces.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlySpaces.cls new file mode 100644 index 00000000..17083d58 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlySpaces.cls @@ -0,0 +1,8 @@ +public class ValidIndentationSpaces { + public void testMultilineString() { + // Only spaces - valid + String testMultiLine = ''' + [hello + world]'''; + } +} diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlyTabs.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlyTabs.cls new file mode 100644 index 00000000..3c0510c9 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_OnlyTabs.cls @@ -0,0 +1,8 @@ +public class ValidIndentationTabs { + public void testMultilineString() { + // Only tabs - valid + String testMultiLine = ''' + [hello + world]'''; + } +} From c32287b50d498e8096f38ec71a3bd109586baa2f Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Thu, 12 Mar 2026 18:55:09 +0530 Subject: [PATCH 2/3] Add comprehensive tests for NoMixedIndentation rule Adds extensive test coverage to ensure no false positives: New test files: - validIndentation_NoFalseFlags.cls - Tests 8 valid patterns: * Code at column 0 (no indentation) * Empty lines * Tabs/spaces in string literals (not indentation) * Mid-line whitespace (after code starts) * Consistent spaces throughout * Consistent tabs throughout * Comments with consistent indentation * Multiline strings with consistent indentation - mixedIndentation_EdgeCases.cls - Tests edge case violations: * Single space + tab * Multiple spaces + tab * Tab + single space * Tab + multiple spaces * Multiple tabs and spaces mixed * Mixed indentation in comments * Mixed indentation in multiline strings New unit tests (6 total): - 2 positive tests (detect violations) - 4 negative tests (no false flags) All 74 tests passing with 100% coverage --- .../test/engine.test.ts | 24 +++++++ .../mixedIndentation_EdgeCases.cls | 39 +++++++++++ .../validIndentation_NoFalseFlags.cls | 65 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_EdgeCases.cls create mode 100644 packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_NoFalseFlags.cls diff --git a/packages/code-analyzer-regex-engine/test/engine.test.ts b/packages/code-analyzer-regex-engine/test/engine.test.ts index aa66fcc2..e144c7c3 100644 --- a/packages/code-analyzer-regex-engine/test/engine.test.ts +++ b/packages/code-analyzer-regex-engine/test/engine.test.ts @@ -1005,6 +1005,30 @@ describe('Tests for runRules', () => { expect(runResults.violations).toHaveLength(0); }); + + it("NoMixedIndentation rule should NOT create false flags for valid patterns", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_NoFalseFlags.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + // Should have 0 violations - all patterns are valid + expect(runResults.violations).toHaveLength(0); + }); + + it("NoMixedIndentation rule should detect edge case violations", async () => { + const runOptions: RunOptions = createRunOptions( + new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_EdgeCases.cls")])); + const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions); + + // Should detect multiple violations + expect(runResults.violations.length).toBeGreaterThan(0); + + // All violations should be NoMixedIndentation + for (const violation of runResults.violations) { + expect(violation.ruleName).toBe("NoMixedIndentation"); + expect(violation.message).toBe(getMessage('MixedIndentationRuleMessage')); + } + }); }); describe('Tests for getEngineVersion', () => { diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_EdgeCases.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_EdgeCases.cls new file mode 100644 index 00000000..d1cf52d8 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/mixedIndentation_EdgeCases.cls @@ -0,0 +1,39 @@ +public class EdgeCaseViolations { + // This file SHOULD trigger violations + + // 1. Single space then tab + public void violation1() { + System.debug('test'); + } + + // 2. Multiple spaces then tab + public void violation2() { + System.debug('test'); + } + + // 3. Tab then single space + public void violation3() { + System.debug('test'); + } + + // 4. Tab then multiple spaces + public void violation4() { + System.debug('test'); + } + + // 5. Multiple tabs and spaces mixed + public void violation5() { + System.debug('test'); + } + + // 6. Mixed indentation in comments + // This comment violates + + // 7. Mixed indentation in multiline string + public void violation6() { + String badString = ''' + This line has spaces then tab + This line has tab then spaces + '''; + } +} diff --git a/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_NoFalseFlags.cls b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_NoFalseFlags.cls new file mode 100644 index 00000000..01095e29 --- /dev/null +++ b/packages/code-analyzer-regex-engine/test/test-data/apexClassMixedIndentation/validIndentation_NoFalseFlags.cls @@ -0,0 +1,65 @@ +public class NoFalseFlagsTest { + // This file should NOT trigger any violations + + // 1. Code starting at column 0 (no indentation) +public void methodOne() { +String x = 'test'; +} + + // 2. Empty lines (should be ignored) + + + // 3. Tabs and spaces in string literals (NOT indentation) + public void stringLiterals() { + String withTab = 'hello world'; // Tab inside string - OK + String withSpaces = 'hello world'; // Spaces inside string - OK + String mixed = 'a b c'; // Mixed inside string - OK + } + + // 4. Whitespace in the middle of lines (after code starts) + public void midLineWhitespace() { + Integer x = 5; // Tab after code starts - OK + String y = 'test'; // Spaces after code starts - OK + } + + // 5. Consistent spaces throughout + public void allSpaces() { + if (true) { + while (false) { + System.debug('test'); + } + } + } + + // 6. Consistent tabs throughout + public void allTabs() { + if (true) { + while (false) { + System.debug('test'); + } + } + } + + // 7. Comments with consistent indentation + public void commentsOk() { + // This comment has spaces + /* Multi-line comment + with spaces */ + System.debug('ok'); + } + + // 8. Multiline strings with consistent indentation + public void multilineStrings() { + String spaces = ''' + Line 1 + Line 2 + Line 3 + '''; + + String tabs = ''' + Line 1 + Line 2 + Line 3 + '''; + } +} From 0d302d8634091231adaa5fb9b7f9079981317146 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Mon, 16 Mar 2026 09:35:29 +0530 Subject: [PATCH 3/3] update textx as per CX --- packages/code-analyzer-regex-engine/src/messages.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/code-analyzer-regex-engine/src/messages.ts b/packages/code-analyzer-regex-engine/src/messages.ts index ceeb454a..85c0a33e 100644 --- a/packages/code-analyzer-regex-engine/src/messages.ts +++ b/packages/code-analyzer-regex-engine/src/messages.ts @@ -44,10 +44,10 @@ const MESSAGE_CATALOG : { [key: string]: string } = { `Found trailing whitespace at the end of a line of code.`, MixedIndentationRuleDescription: - `Detects lines where leading indentation contains both spaces and tabs. Mixed indentation can cause unexpected behavior in multiline strings (introduced in Apex API v262) due to how the indentation algorithm processes whitespace.`, + `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.`, MixedIndentationRuleMessage: - `Found mixed spaces and tabs in leading indentation. Use either spaces or tabs consistently for indentation, not both.`, + `Leading indentation contains mixed spaces and tabs. To fix this, use either spaces or tabs.`, AvoidTermsWithImplicitBiasRuleDescription: `"Detects usage of terms that reinforce implicit bias.`,