Skip to content

Commit

Permalink
NEW (RegexEngine) @W-15985046@ Implement regex engine with trailing w…
Browse files Browse the repository at this point in the history
…hitespace rule (#34)
  • Loading branch information
ravipanguluri authored Jul 2, 2024
1 parent 39d669d commit 1f269c1
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 24 deletions.
22 changes: 13 additions & 9 deletions packages/code-analyzer-regex-engine/src/RegexEnginePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
RuleDescription,
RuleType,
RunOptions,
SeverityLevel
SeverityLevel,
Violation
} from "@salesforce/code-analyzer-engine-api";
import { RegexExecutor } from './executor';

export class RegexEnginePlugin extends EnginePluginV1 {

Expand Down Expand Up @@ -39,16 +41,18 @@ export class RegexEngine extends Engine {
severityLevel: SeverityLevel.Low,
type: RuleType.Standard,
tags: ["Recommended", "CodeStyle"],
/* TODO: Add rule description and resourceUrls for trailing whitespace rule*/
description: "",
resourceUrls: [""]
description: "Detects trailing whitespace (tabs or spaces) at the end of lines of code and lines that are only whitespace.",
resourceUrls: []
},
];
}

async runRules(_ruleNames: string[], _runOptions: RunOptions): Promise<EngineRunResults> {
/* TODO: Update section with logic for implementing trailing whitespace rule*/
return {violations: []};

}
async runRules(_ruleNames: string[], runOptions: RunOptions): Promise<EngineRunResults> {
const executor = new RegexExecutor()
const fullFileList: string[] = await runOptions.workspace.getExpandedFiles()
const violations: Violation[] = await executor.execute(fullFileList)
return {
violations: violations
};
}
}
77 changes: 77 additions & 0 deletions packages/code-analyzer-regex-engine/src/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Violation } from "@salesforce/code-analyzer-engine-api";
import fs from "node:fs";
import path from "node:path";
import os from "node:os"

const APEX_CLASS_FILE_EXT: string = ".cls"

export class RegexExecutor {
lineSep: string

constructor() {
this.lineSep = os.EOL
}

async execute(allFiles: string[]): Promise<Violation[]> {
let violations: Violation[] = []

for (const file of allFiles) {
const fileData = fs.statSync(file)
if (fileData.isFile()) {
violations = violations.concat(await this.scanFile(file));
}
}
return violations;
}

private getColumnNumber(fileContents: string, charIndex: number, newlineIndexes: number[]): number {
/*TODO: swap out findIndex for a modified binary search implementation */
const idxOfNextNewline = newlineIndexes.findIndex(el => el >= charIndex)
const idxOfCurrentLine = idxOfNextNewline === -1 ? newlineIndexes.length - 1: idxOfNextNewline - 1
const eolOffset = this.lineSep.length - 1
return charIndex - newlineIndexes.at(idxOfCurrentLine)! - eolOffset
}

private getLineNumber(fileContents: string, charIndex: number, newlineIndexes: number[]): number{
const idxOfNextNewline = newlineIndexes.findIndex(el => el >= charIndex)
return idxOfNextNewline === -1 ? newlineIndexes.length + 1 : idxOfNextNewline + 1;
}

private async scanFile(fileName: string): Promise<Violation[]> {
const violations: Violation[] = [];
if (path.extname((fileName)) === APEX_CLASS_FILE_EXT) {
const fileContents: string = fs.readFileSync(fileName, {encoding: 'utf8'})
const regex: RegExp = /[ \t]+((?=\r?\n)|(?=$))/g;
const matches = fileContents.matchAll(regex);
const newlineIndexes = this.getNewlineIndices(fileContents);

for (const match of matches) {
const codeLocation = {
file: fileName,
startLine: this.getLineNumber(fileContents, match.index, newlineIndexes),
startColumn: this.getColumnNumber(fileContents, match.index, newlineIndexes),
endLine: this.getLineNumber(fileContents,match.index + match[0].length, newlineIndexes),
endColumn: this.getColumnNumber(fileContents,match.index + match[0].length, newlineIndexes)
}
violations.push({
ruleName: "TrailingWhitespaceRule",
codeLocations: [codeLocation],
primaryLocationIndex: 0,
message: "Detects trailing whitespace (tabs or spaces) at the end of lines of code and lines that are only whitespace."
});
}
}
return violations;
}

private getNewlineIndices(fileContents: string): number[] {
const newlineRegex: RegExp = new RegExp(this.lineSep, "g")
const matches = fileContents.matchAll(newlineRegex);
const newlineIndexes = []

for (const match of matches) {
newlineIndexes.push(match.index);
}
return newlineIndexes
}
}
59 changes: 44 additions & 15 deletions packages/code-analyzer-regex-engine/test/engine.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import {RegexEnginePlugin, RegexEngine} from "../src/RegexEnginePlugin";
import path from "node:path";
import {changeWorkingDirectoryToPackageRoot} from "./test-helpers";
import {
RuleDescription,
RuleType,
SeverityLevel
SeverityLevel,
RunOptions,
EngineRunResults,
Violation
} from "@salesforce/code-analyzer-engine-api";
import * as testTools from "@salesforce/code-analyzer-engine-api/testtools"
import { TRAILING_WHITESPACE_RULE_MESSAGE, TRAILING_WHITESPACE_RESOURCE_URLS, EXPECTED_VIOLATION_2 } from "./test-config";

changeWorkingDirectoryToPackageRoot();

describe('Regex Engine Tests', () => {
let engine: RegexEngine;
let engine: RegexEngine;
beforeAll(() => {
engine = new RegexEngine();
});
Expand All @@ -29,16 +34,44 @@ describe('Regex Engine Tests', () => {
severityLevel: SeverityLevel.Low,
type: RuleType.Standard,
tags: ["Recommended", "CodeStyle"],
description: "",
resourceUrls: [""]
description: TRAILING_WHITESPACE_RULE_MESSAGE,
resourceUrls: TRAILING_WHITESPACE_RESOURCE_URLS
},
];
expect(rules_desc).toEqual(engineRules)
});

it('Confirm runRules() is a no-op', () => {
const ruleNames: string[] = ['TrailingWhitespaceRule']
engine.runRules(ruleNames, {workspace: testTools.createWorkspace([])});


describe('runRules() tests', () => {
let ruleNames: string[];
beforeAll(() => {
ruleNames = ['TrailingWhitespaceRule']
})

it('if runRules() is called on a directory with no apex files, it should correctly return no violations', async () => {
const filePath = path.resolve("test", "test-data", "1_notApexClassWithWhitespace")
const runOptions: RunOptions = {workspace: testTools.createWorkspace([filePath])}
const runResults: EngineRunResults = await engine.runRules(ruleNames, runOptions);
const expViolations: Violation[] = [];
expect(runResults.violations).toStrictEqual(expViolations);
});

it('Confirm runRules() returns correct errors when called on a file', async () => {
const ruleNames: string[] = ['TrailingWhitespaceRule']
const filePath = path.resolve("test", "test-data", "2_apexClasses", "myClass.cls")
const runOptions: RunOptions = {workspace: testTools.createWorkspace([filePath])}
const runResults: EngineRunResults = await engine.runRules(ruleNames, runOptions);
expect(runResults.violations).toStrictEqual(EXPECTED_VIOLATION_2)
});

it('If runRules() finds no violations when an apex file has no trailing whitespaces', async () => {
const filePath = path.resolve("test", "test-data", "4_ApexClassWithoutWhitespace")
const runOptions: RunOptions = {workspace: testTools.createWorkspace([filePath])}
const runResults: EngineRunResults = await engine.runRules(ruleNames, runOptions);
const expViolations: Violation[] = [];
expect(runResults.violations).toStrictEqual(expViolations);
});
})
});

Expand Down Expand Up @@ -67,20 +100,16 @@ describe('RegexEnginePlugin Tests' , () => {
severityLevel: SeverityLevel.Low,
type: RuleType.Standard,
tags: ["Recommended", "CodeStyle"],
description: "",
resourceUrls: [""]
description: "Detects trailing whitespace (tabs or spaces) at the end of lines of code and lines that are only whitespace.",
resourceUrls: []
},
];
const engineRules: RuleDescription[] = await pluginEngine.describeRules({workspace: testTools.createWorkspace([])})
expect(engineRules).toStrictEqual(expEngineRules)
});

it('Check that engine created from the RegexEnginePlugin has runRules() method as a no-op', () => {
const ruleNames: string[] = ['TrailingWhitespaceRule']
pluginEngine.runRules(ruleNames, {workspace: testTools.createWorkspace([])});
});

it('If I make an engine with an invalid name, it should throw an error with the proper error message', () => {
expect(enginePlugin.createEngine('OtherEngine', {})).rejects.toThrow("Unsupported engine name: OtherEngine");
});
});
});

57 changes: 57 additions & 0 deletions packages/code-analyzer-regex-engine/test/executor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

import { changeWorkingDirectoryToPackageRoot } from "./test-helpers";
import path from "node:path";
import { RegexExecutor } from '../src/executor';
import {
EXPECTED_VIOLATION_1,
EXPECTED_VIOLATION_2,
EXPECTED_VIOLATION_3
} from './test-config';
import { Violation } from "@salesforce/code-analyzer-engine-api";

changeWorkingDirectoryToPackageRoot();

describe("Executor tests", () => {
let executor: RegexExecutor;
beforeAll(() => {
executor = new RegexExecutor();
});

/* I realize that I am not totally certain what the intended behavior of this case should be. But it might be better as just a no-op*/
it("If I have a file that's not an Apex class, execute() should not return any violations.", async () =>
{
const file = path.resolve("test", "test-data", "1_notApexClassWithWhitespace", "something.xml")
const violations: Violation[] = await executor.execute([file])
const expViolations: Violation[] = [];
expect(violations).toStrictEqual(expViolations);
});

it("If execute() is called with an Apex class that has trailing whitespace, emit violation", async () => {
const file = path.resolve("test", "test-data", "2_apexClasses", "myOuterClass.cls")
const violations: Violation[] = await executor.execute([file])

expect(violations).toStrictEqual(EXPECTED_VIOLATION_1)
});

it("If execute() is pointed to an Apex class without trailing whitespace ensure there are no erroneous violations", async () => {
const file = path.resolve("test", "test-data", "4_ApexClassWithoutWhitespace", "myOuterClass.cls")
const violations: Violation[] = await executor.execute([file])
const expViolations: Violation[] = [];
expect(violations).toStrictEqual(expViolations);

});

it('Ensure that execute() can catch multiple violations in the same file', async () => {
const file = path.resolve("test", "test-data", "2_apexClasses", "myClass.cls");
const violations: Violation[] = await executor.execute([file]);
expect(violations).toStrictEqual(EXPECTED_VIOLATION_2)
})

it("Ensure execute() can be called on a list Apex classes and properly emits errors", async () => {
const file1 = path.resolve("test", "test-data", "2_apexClasses", "myOuterClass.cls")
const file2 = path.resolve("test", "test-data", "2_apexClasses", "myClass.cls");
const file3 = path.resolve("test", "test-data", "4_ApexClassWithoutWhitespace", "myOuterClass.cls")
const violations: Violation[] = await executor.execute([file1, file2, file3])
expect(violations).toStrictEqual(EXPECTED_VIOLATION_3)
});
})
83 changes: 83 additions & 0 deletions packages/code-analyzer-regex-engine/test/test-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import path from "node:path";
import { changeWorkingDirectoryToPackageRoot } from "./test-helpers";
import { CodeLocation, Violation} from "@salesforce/code-analyzer-engine-api";

changeWorkingDirectoryToPackageRoot();

export const FILE_LOCATION_1 = path.resolve(__dirname, "test-data", "2_apexClasses", "myOuterClass.cls")
export const FILE_LOCATION_2 = path.resolve(__dirname, "test-data", "2_apexClasses", "myClass.cls")
export const TRAILING_WHITESPACE_RULE_NAME = "TrailingWhitespaceRule"
export const TRAILING_WHITESPACE_RULE_MESSAGE = "Detects trailing whitespace (tabs or spaces) at the end of lines of code and lines that are only whitespace.";
export const TRAILING_WHITESPACE_RESOURCE_URLS = []

const EXPECTED_CODE_LOCATION_1: CodeLocation = {
file: FILE_LOCATION_1,
startLine: 6,
startColumn: 2,
endLine: 6,
endColumn: 4
}

const EXPECTED_CODE_LOCATION_2: CodeLocation = {
file: FILE_LOCATION_2,
startLine: 2,
startColumn: 40,
endLine: 2,
endColumn: 41
}

const EXPECTED_CODE_LOCATION_3: CodeLocation = {
file: FILE_LOCATION_2,
startLine: 6,
startColumn: 2,
endLine: 6,
endColumn: 4
}

export const EXPECTED_VIOLATION_1: Violation[] = [
{
ruleName: TRAILING_WHITESPACE_RULE_NAME,
message: TRAILING_WHITESPACE_RULE_MESSAGE,
primaryLocationIndex: 0,
codeLocations: [EXPECTED_CODE_LOCATION_1]
}
]

export const EXPECTED_VIOLATION_2: Violation[] = [
{
ruleName: TRAILING_WHITESPACE_RULE_NAME,
message: TRAILING_WHITESPACE_RULE_MESSAGE,
primaryLocationIndex: 0,
codeLocations: [EXPECTED_CODE_LOCATION_2]
},
{
ruleName: TRAILING_WHITESPACE_RULE_NAME,
message: TRAILING_WHITESPACE_RULE_MESSAGE,
primaryLocationIndex: 0,
codeLocations: [EXPECTED_CODE_LOCATION_3]

}
]

export const EXPECTED_VIOLATION_3: Violation[] = [
{
ruleName: TRAILING_WHITESPACE_RULE_NAME,
message: TRAILING_WHITESPACE_RULE_MESSAGE,
primaryLocationIndex: 0,
codeLocations: [EXPECTED_CODE_LOCATION_1]
},
{
ruleName: TRAILING_WHITESPACE_RULE_NAME,
message: TRAILING_WHITESPACE_RULE_MESSAGE,
primaryLocationIndex: 0,
codeLocations: [EXPECTED_CODE_LOCATION_2]
},
{
ruleName: TRAILING_WHITESPACE_RULE_NAME,
message: TRAILING_WHITESPACE_RULE_MESSAGE,
primaryLocationIndex: 0,
codeLocations: [EXPECTED_CODE_LOCATION_3]

}
]

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<root>
<describe horn="out">
<!--model before raise lot outer-->main
</describe>
<history include="bush">
<cut game="slowly">excitement</cut>
<suppose very="someone">wood</suppose>
<four hand="stop">
<!--fine-->-807916209.3514104
</four>
</history>
<!--truth stems plastic-->
<chose>-927308825</chose>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public class myCls {
// Additional myOuterClass code here
class myInnerClass {
// myInnerClass code here
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public class myOuterClass {
// Additional myOuterClass code here
class myInnerClass {
// myInnerClass code here
}
}
Loading

0 comments on commit 1f269c1

Please sign in to comment.