Skip to content

Commit 9f67068

Browse files
authored
NEW @W-19374413@ Derive newline from contents instead of OS (#332)
1 parent 6cc4f08 commit 9f67068

File tree

6 files changed

+80
-12
lines changed

6 files changed

+80
-12
lines changed

.gitattributes

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
# We do not wish to have GitHub add carriage returns for these files on windows.
33
# So we forcefully set any file that ends in .goldfile.<ext> to only have \n characters:
44
# See https://help.github.com/articles/dealing-with-line-endings/
5-
*.goldfile.* text eol=lf
5+
*.goldfile.* text eol=lf
6+
7+
# Some of our input files must be forced to use a certain EOL signifier no matter what the OS wants.
8+
# So we forcefully set any file that includes lf or crlf in its file extension to use the corresponding EOL.
9+
*.lf.* text eol=lf
10+
*.crlf.* text eol=crlf

packages/code-analyzer-regex-engine/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-regex-engine",
33
"description": "Plugin package that adds 'regex' as an engine into Salesforce Code Analyzer",
4-
"version": "0.25.0",
4+
"version": "0.25.0-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
@@ -62,4 +62,4 @@
6262
"!src/index.ts"
6363
]
6464
}
65-
}
65+
}

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
import path from "node:path";
1212
import fs from "node:fs";
1313
import * as fsp from 'node:fs/promises';
14-
import os from "node:os";
1514
import {RegexRule, RegexRules} from "./config";
1615
import {isBinaryFile} from "isbinaryfile";
1716
import {convertToRegex, PromiseExecutionLimiter} from "./utils";
@@ -137,7 +136,8 @@ export class RegexEngine extends Engine {
137136
private async scanFileContents(fileName: string, fileContents: string, ruleName: string): Promise<Violation[]> {
138137
const violations: Violation[] = [];
139138
const regex: RegExp = this.getRegExpFor(ruleName);
140-
const newlineIndexes: number[] = getNewlineIndices(fileContents);
139+
const contextuallyDerivedEol: string = contextuallyDeriveEolString(fileContents);
140+
const newlineIndexes: number[] = getNewlineIndices(fileContents, contextuallyDerivedEol);
141141

142142
for (const match of fileContents.matchAll(regex)) {
143143
let startIndex: number = match.index;
@@ -151,9 +151,9 @@ export class RegexEngine extends Engine {
151151
}
152152

153153
const startLine: number = getLineNumber(startIndex, newlineIndexes);
154-
const startColumn: number = getColumnNumber(startIndex, newlineIndexes);
154+
const startColumn: number = getColumnNumber(startIndex, newlineIndexes, contextuallyDerivedEol);
155155
const endLine: number = getLineNumber(startIndex + matchLength, newlineIndexes);
156-
const endColumn: number = getColumnNumber(startIndex + matchLength, newlineIndexes);
156+
const endColumn: number = getColumnNumber(startIndex + matchLength, newlineIndexes, contextuallyDerivedEol);
157157
const codeLocation: CodeLocation = {
158158
file: fileName,
159159
startLine: startLine,
@@ -180,13 +180,13 @@ export class RegexEngine extends Engine {
180180
}
181181

182182

183-
function getColumnNumber(charIndex: number, newlineIndexes: number[]): number {
183+
function getColumnNumber(charIndex: number, newlineIndexes: number[], contextuallyDerivedEol: string): number {
184184
const idxOfNextNewline = newlineIndexes.findIndex(el => el >= charIndex);
185185
const idxOfCurrentLine = idxOfNextNewline === -1 ? newlineIndexes.length - 1: idxOfNextNewline - 1;
186186
if (idxOfCurrentLine === 0){
187187
return charIndex + 1;
188188
} else {
189-
const eolOffset = os.EOL.length - 1;
189+
const eolOffset = contextuallyDerivedEol.length - 1;
190190
return charIndex - newlineIndexes.at(idxOfCurrentLine)! - eolOffset;
191191
}
192192
}
@@ -196,8 +196,8 @@ function getLineNumber(charIndex: number, newlineIndexes: number[]): number{
196196
return idxOfNextNewline === -1 ? newlineIndexes.length : idxOfNextNewline;
197197
}
198198

199-
function getNewlineIndices(fileContents: string): number[] {
200-
const newlineRegex: RegExp = new RegExp(os.EOL, "g");
199+
function getNewlineIndices(fileContents: string, contextuallyDerivedEol: string): number[] {
200+
const newlineRegex: RegExp = new RegExp(contextuallyDerivedEol, "g");
201201
const matches = fileContents.matchAll(newlineRegex);
202202
const newlineIndexes = [-1];
203203

@@ -207,6 +207,15 @@ function getNewlineIndices(fileContents: string): number[] {
207207
return newlineIndexes;
208208
}
209209

210+
/**
211+
* We can't assume that the file's EOL indicator matches the OS's, because of edge cases such as a file created on Unix
212+
* but scanned on Windows. So instead of consulting the OS for the EOL indicator, we look at the file and find which one it uses.
213+
* @param contents
214+
*/
215+
function contextuallyDeriveEolString(contents: string): string {
216+
return contents.indexOf('\r\n') !== -1 ? '\r\n' : '\n';
217+
}
218+
210219
async function isTextFile(fileName: string): Promise<boolean> {
211220
const ext: string = path.extname(fileName).toLowerCase();
212221
return TEXT_BASED_FILE_EXTS.has(ext) || !(await isBinaryFile(fileName));

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const SAMPLE_CUSTOM_RULES: RegexRules = {
3939
NoTalkingAboutFightClub: {
4040
regex: '/fight club/gi',
4141
description: "The first rule of Fight Club is do not talk about Fight Club",
42-
file_extensions: [".abc-def.xyz"],
42+
file_extensions: [".abc-def.xyz", ".lf.cls", ".crlf.cls"],
4343
violation_message: "The second rule of Fight Club is DO NOT. TALK. ABOUT FIGHT CLUB.",
4444
severity: DEFAULT_SEVERITY_LEVEL,
4545
tags: ['PopCulture']
@@ -783,6 +783,48 @@ describe('Tests for runRules', () => {
783783
}
784784
});
785785

786+
it('When files indicate EOL differently than OS, violations are still correct', async () => {
787+
const runOptions: RunOptions = createRunOptions(
788+
new Workspace('id', [path.resolve(__dirname, "test-data", "workspaceWithOddNewlines")]));
789+
const runResults: EngineRunResults = await engine.runRules(['NoTalkingAboutFightClub'], runOptions);
790+
791+
const expectedViolations: Violation[] = [
792+
{
793+
ruleName: "NoTalkingAboutFightClub",
794+
message: "The second rule of Fight Club is DO NOT. TALK. ABOUT FIGHT CLUB.",
795+
primaryLocationIndex: 0,
796+
codeLocations: [
797+
{
798+
file: path.resolve(__dirname, "test-data", "workspaceWithOddNewlines", "testClass.lf.cls"),
799+
startLine: 3,
800+
startColumn: 87,
801+
endLine: 3,
802+
endColumn: 97
803+
}
804+
]
805+
},
806+
{
807+
ruleName: "NoTalkingAboutFightClub",
808+
message: "The second rule of Fight Club is DO NOT. TALK. ABOUT FIGHT CLUB.",
809+
primaryLocationIndex: 0,
810+
codeLocations: [
811+
{
812+
file: path.resolve(__dirname, "test-data", "workspaceWithOddNewlines", "testClass.crlf.cls"),
813+
startLine: 3,
814+
startColumn: 87,
815+
endLine: 3,
816+
endColumn: 97
817+
}
818+
]
819+
}
820+
];
821+
822+
expect(runResults.violations).toHaveLength(expectedViolations.length);
823+
for (const expectedViolation of expectedViolations) {
824+
expect(runResults.violations).toContainEqual(expectedViolation);
825+
}
826+
});
827+
786828
it("When running all rules compared to some rules, then output correctly returns what the correct violations according to specified rules", async () => {
787829
const runOptions: RunOptions = createRunOptions(
788830
new Workspace('id', [path.resolve(__dirname, "test-data", "sampleWorkspace")]));
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public class TestClass{
2+
public static boolean doTheThing() {
3+
List<String> strings = new List<String>{'This is fine', 'This is also fine', 'fight club', 'OH NO, that last string was bad!'};
4+
return false;
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public class TestClass {
2+
public static boolean doTheThing() {
3+
List<String> strings = new List<String>{'THis is fine', 'THis is also fine', 'fight club', 'OH NO, that last string was bad!'};
4+
return false;
5+
}
6+
}

0 commit comments

Comments
 (0)