Skip to content

Commit 44413a8

Browse files
aruntyagiTutunikhil-mittal-165
authored andcommitted
NEW @W-21521130@ - Add NoMixedIndentation rule to detect mixed tabs and spaces in Apex indentation (#437)
1 parent e0ce88c commit 44413a8

9 files changed

Lines changed: 235 additions & 15 deletions

File tree

packages/code-analyzer-regex-engine/src/messages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ const MESSAGE_CATALOG : { [key: string]: string } = {
4343
TrailingWhitespaceRuleMessage:
4444
`Found trailing whitespace at the end of a line of code.`,
4545

46+
MixedIndentationRuleDescription:
47+
`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.`,
48+
49+
MixedIndentationRuleMessage:
50+
`Leading indentation contains mixed spaces and tabs. To fix this, use either spaces or tabs.`,
51+
4652
AvoidTermsWithImplicitBiasRuleDescription:
4753
`"Detects usage of terms that reinforce implicit bias.`,
4854

packages/code-analyzer-regex-engine/src/plugin.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ export function createBaseRegexRules(now: Date): RegexRules {
7979
severity: SeverityLevel.Info,
8080
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX]
8181
},
82+
NoMixedIndentation: {
83+
regex: (/^(?<target>[ \t]*(\t[ ]+|[ ]+\t)[ \t]*)/gm).toString(),
84+
file_extensions: ['.cls', '.trigger'],
85+
description: getMessage('MixedIndentationRuleDescription'),
86+
violation_message: getMessage('MixedIndentationRuleMessage'),
87+
severity: SeverityLevel.Moderate,
88+
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX]
89+
},
8290
AvoidTermsWithImplicitBias: { // file_extensions not listed so that it can run on all text files
8391
regex: (/\b(((black|white)\s*list\w*)|((black|brown)\s*out\w*)|(slaves?\b))/gi).toString(),
8492
description: getMessage('AvoidTermsWithImplicitBiasRuleDescription'),

packages/code-analyzer-regex-engine/test/engine.test.ts

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ const EXPECTED_NoTrailingWhitespace_RULE_DESCRIPTION: RuleDescription = {
5252
resourceUrls: []
5353
};
5454

55+
const EXPECTED_NoMixedIndentation_RULE_DESCRIPTION: RuleDescription = {
56+
name: "NoMixedIndentation",
57+
severityLevel: SeverityLevel.Moderate,
58+
tags: ["Recommended", "CodeStyle", "Apex"],
59+
description: getMessage('MixedIndentationRuleDescription'),
60+
resourceUrls: []
61+
};
62+
5563
const EXPECTED_NoTodos_RULE_DESCRIPTION: RuleDescription = {
5664
name: "NoTodos",
5765
severityLevel: DEFAULT_SEVERITY_LEVEL,
@@ -126,15 +134,16 @@ describe("Tests for RegexEngine's getName and describeRules methods", () => {
126134

127135
it('Calling describeRules without workspace, returns all available rules', async () => {
128136
const rulesDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions());
129-
expect(rulesDescriptions).toHaveLength(8);
137+
expect(rulesDescriptions).toHaveLength(9);
130138
expect(rulesDescriptions[0]).toMatchObject(EXPECTED_NoTrailingWhitespace_RULE_DESCRIPTION);
131-
expect(rulesDescriptions[1]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION)
132-
expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION)
133-
expect(rulesDescriptions[3]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION)
134-
expect(rulesDescriptions[4]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION)
135-
expect(rulesDescriptions[5]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION);
136-
expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION);
137-
expect(rulesDescriptions[7]).toMatchObject(EXPECTED_NoTalkingAboutFightClub_RULE_DESCRIPTION);
139+
expect(rulesDescriptions[1]).toMatchObject(EXPECTED_NoMixedIndentation_RULE_DESCRIPTION);
140+
expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION);
141+
expect(rulesDescriptions[3]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION);
142+
expect(rulesDescriptions[4]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION);
143+
expect(rulesDescriptions[5]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION);
144+
expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION);
145+
expect(rulesDescriptions[7]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION);
146+
expect(rulesDescriptions[8]).toMatchObject(EXPECTED_NoTalkingAboutFightClub_RULE_DESCRIPTION);
138147
});
139148

140149
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", () => {
157166
it("When workspace contains files are applicable to all available rules, then describeRules returns all rules", async () => {
158167
const rulesDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions(
159168
new Workspace('id', [path.resolve(__dirname, 'test-data', 'sampleWorkspace')])));
160-
expect(rulesDescriptions).toHaveLength(7);
169+
expect(rulesDescriptions).toHaveLength(8);
161170
expect(rulesDescriptions[0]).toMatchObject(EXPECTED_NoTrailingWhitespace_RULE_DESCRIPTION);
162-
expect(rulesDescriptions[1]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION);
163-
expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION);
164-
expect(rulesDescriptions[3]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION);
165-
expect(rulesDescriptions[4]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION);
166-
expect(rulesDescriptions[5]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION);
167-
expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION);
171+
expect(rulesDescriptions[1]).toMatchObject(EXPECTED_NoMixedIndentation_RULE_DESCRIPTION);
172+
expect(rulesDescriptions[2]).toMatchObject(EXPECTED_AvoidTermsWithImplicitBias_RULE_DESCRIPTION);
173+
expect(rulesDescriptions[3]).toMatchObject(EXPECTED_AvoidOldSalesforceApiVersions_RULE_DESCRIPTION);
174+
expect(rulesDescriptions[4]).toMatchObject(EXPECTED_NoGetHeapSizeInLoop_RULE_DESCRIPTION);
175+
expect(rulesDescriptions[5]).toMatchObject(EXPECTED_MinVersionForAbstractVirtualClassesWithPrivateMethod_RULE_DESCRIPTION);
176+
expect(rulesDescriptions[6]).toMatchObject(EXPECTED_NoTodos_RULE_DESCRIPTION);
177+
expect(rulesDescriptions[7]).toMatchObject(EXPECTED_NoHellos_RULE_DESCRIPTION);
168178
});
169179
});
170180

@@ -959,6 +969,66 @@ describe('Tests for runRules', () => {
959969
expect(combinedRunViolations).toContainEqual(individualRunViolation);
960970
}
961971
});
972+
973+
it("NoMixedIndentation rule should detect tab followed by spaces in indentation", async () => {
974+
const runOptions: RunOptions = createRunOptions(
975+
new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_TabThenSpaces.cls")]));
976+
const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions);
977+
978+
expect(runResults.violations.length).toBeGreaterThan(0);
979+
expect(runResults.violations[0].ruleName).toBe("NoMixedIndentation");
980+
expect(runResults.violations[0].message).toBe(getMessage('MixedIndentationRuleMessage'));
981+
});
982+
983+
it("NoMixedIndentation rule should detect spaces followed by tab in indentation", async () => {
984+
const runOptions: RunOptions = createRunOptions(
985+
new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_SpacesThenTab.cls")]));
986+
const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions);
987+
988+
expect(runResults.violations.length).toBeGreaterThan(0);
989+
expect(runResults.violations[0].ruleName).toBe("NoMixedIndentation");
990+
expect(runResults.violations[0].message).toBe(getMessage('MixedIndentationRuleMessage'));
991+
});
992+
993+
it("NoMixedIndentation rule should NOT flag lines with only spaces", async () => {
994+
const runOptions: RunOptions = createRunOptions(
995+
new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_OnlySpaces.cls")]));
996+
const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions);
997+
998+
expect(runResults.violations).toHaveLength(0);
999+
});
1000+
1001+
it("NoMixedIndentation rule should NOT flag lines with only tabs", async () => {
1002+
const runOptions: RunOptions = createRunOptions(
1003+
new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_OnlyTabs.cls")]));
1004+
const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions);
1005+
1006+
expect(runResults.violations).toHaveLength(0);
1007+
});
1008+
1009+
it("NoMixedIndentation rule should NOT create false flags for valid patterns", async () => {
1010+
const runOptions: RunOptions = createRunOptions(
1011+
new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "validIndentation_NoFalseFlags.cls")]));
1012+
const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions);
1013+
1014+
// Should have 0 violations - all patterns are valid
1015+
expect(runResults.violations).toHaveLength(0);
1016+
});
1017+
1018+
it("NoMixedIndentation rule should detect edge case violations", async () => {
1019+
const runOptions: RunOptions = createRunOptions(
1020+
new Workspace('id', [path.resolve(__dirname, "test-data", "apexClassMixedIndentation", "mixedIndentation_EdgeCases.cls")]));
1021+
const runResults: EngineRunResults = await engine.runRules(["NoMixedIndentation"], runOptions);
1022+
1023+
// Should detect multiple violations
1024+
expect(runResults.violations.length).toBeGreaterThan(0);
1025+
1026+
// All violations should be NoMixedIndentation
1027+
for (const violation of runResults.violations) {
1028+
expect(violation.ruleName).toBe("NoMixedIndentation");
1029+
expect(violation.message).toBe(getMessage('MixedIndentationRuleMessage'));
1030+
}
1031+
});
9621032
});
9631033

9641034
describe('Tests for getEngineVersion', () => {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
public class EdgeCaseViolations {
2+
// This file SHOULD trigger violations
3+
4+
// 1. Single space then tab
5+
public void violation1() {
6+
System.debug('test');
7+
}
8+
9+
// 2. Multiple spaces then tab
10+
public void violation2() {
11+
System.debug('test');
12+
}
13+
14+
// 3. Tab then single space
15+
public void violation3() {
16+
System.debug('test');
17+
}
18+
19+
// 4. Tab then multiple spaces
20+
public void violation4() {
21+
System.debug('test');
22+
}
23+
24+
// 5. Multiple tabs and spaces mixed
25+
public void violation5() {
26+
System.debug('test');
27+
}
28+
29+
// 6. Mixed indentation in comments
30+
// This comment violates
31+
32+
// 7. Mixed indentation in multiline string
33+
public void violation6() {
34+
String badString = '''
35+
This line has spaces then tab
36+
This line has tab then spaces
37+
''';
38+
}
39+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public class MixedIndentationExample {
2+
public void testMultilineString() {
3+
// This has mixed indentation: spaces followed by tab
4+
String testMultiLine = '''
5+
[hello
6+
world]''';
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public class MixedIndentationExample {
2+
public void testMultilineString() {
3+
// This has mixed indentation: tab followed by spaces
4+
String testMultiLine = '''
5+
[hello
6+
world]''';
7+
}
8+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
public class NoFalseFlagsTest {
2+
// This file should NOT trigger any violations
3+
4+
// 1. Code starting at column 0 (no indentation)
5+
public void methodOne() {
6+
String x = 'test';
7+
}
8+
9+
// 2. Empty lines (should be ignored)
10+
11+
12+
// 3. Tabs and spaces in string literals (NOT indentation)
13+
public void stringLiterals() {
14+
String withTab = 'hello world'; // Tab inside string - OK
15+
String withSpaces = 'hello world'; // Spaces inside string - OK
16+
String mixed = 'a b c'; // Mixed inside string - OK
17+
}
18+
19+
// 4. Whitespace in the middle of lines (after code starts)
20+
public void midLineWhitespace() {
21+
Integer x = 5; // Tab after code starts - OK
22+
String y = 'test'; // Spaces after code starts - OK
23+
}
24+
25+
// 5. Consistent spaces throughout
26+
public void allSpaces() {
27+
if (true) {
28+
while (false) {
29+
System.debug('test');
30+
}
31+
}
32+
}
33+
34+
// 6. Consistent tabs throughout
35+
public void allTabs() {
36+
if (true) {
37+
while (false) {
38+
System.debug('test');
39+
}
40+
}
41+
}
42+
43+
// 7. Comments with consistent indentation
44+
public void commentsOk() {
45+
// This comment has spaces
46+
/* Multi-line comment
47+
with spaces */
48+
System.debug('ok');
49+
}
50+
51+
// 8. Multiline strings with consistent indentation
52+
public void multilineStrings() {
53+
String spaces = '''
54+
Line 1
55+
Line 2
56+
Line 3
57+
''';
58+
59+
String tabs = '''
60+
Line 1
61+
Line 2
62+
Line 3
63+
''';
64+
}
65+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public class ValidIndentationSpaces {
2+
public void testMultilineString() {
3+
// Only spaces - valid
4+
String testMultiLine = '''
5+
[hello
6+
world]''';
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
public class ValidIndentationTabs {
2+
public void testMultilineString() {
3+
// Only tabs - valid
4+
String testMultiLine = '''
5+
[hello
6+
world]''';
7+
}
8+
}

0 commit comments

Comments
 (0)