Skip to content

Commit a3c2ace

Browse files
authored
Fix autofix replacements & rework test pipeline (#14)
* Track dummy replacements & revert them at the end * Also use hash for JS preprocessor * Replace "index" with "id" * Mark variable as unused * Get rid of wrong offsetmap lookup * Update ESLint VSCode settings * Replace cumbersome unit tests by integration tests * Add another fixture: multiple properties on object * Strip unit tests to most essential stuff * Outsource run test method * Add first HTML test * Improve HTML rules & add one more HTML test * Add test to test set * Replace unnecessary map by array
1 parent 780055a commit a3c2ace

38 files changed

+469
-1916
lines changed

.vscode/settings.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22
//////////////////////////////////////
33
// JS (ESLint)
44
//////////////////////////////////////
5-
"eslint.format.enable": true,
6-
"eslint.experimental.useFlatConfig": true,
75
"[javascript]": {
86
"editor.formatOnSave": false, // to avoid formatting twice (ESLint + VSCode)
97
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
108
},
119
"editor.codeActionsOnSave": {
1210
"source.fixAll.eslint": "explicit"
1311
},
14-
// this disables VSCode built-int formatter (instead we want to use ESLint)
12+
"eslint.format.enable": true,
13+
// this disables VSCode built-in formatter (instead we want to use ESLint)
1514
"javascript.validate.enable": false,
15+
"eslint.options": {
16+
"overrideConfigFile": "./eslint.config.js"
17+
},
18+
"eslint.validate": [
19+
"javascript"
20+
],
1621
//////////////////////////////////////
1722
// Git
1823
//////////////////////////////////////

lib/autofix.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const { coordinatesToIndex, indexToColumn } = require("./file_coordinates.js");
22
const { calculateOffset } = require("./offset_calculation.js");
33

4-
function transformFix(message, originalText, lintableText, offsetMap) {
4+
function transformFix(message, originalText, lintableText, offsetMap, dummyReplacementsMap) {
55
const newRange = calculateNewRange(message, originalText, lintableText, offsetMap);
6-
const newFixText = transformFixText(message, newRange, originalText);
6+
const newFixText = transformFixText(message, newRange, originalText,
7+
offsetMap, dummyReplacementsMap);
78

89
return {
910
range: newRange,
@@ -35,19 +36,9 @@ function calculateNewRange(message, originalText, lintableText, offsetMap) {
3536
});
3637
}
3738

38-
function transformFixText(message, newRange, originalText) {
39+
function transformFixText(message, newRange, originalText, offsetMap, dummyReplacementsMap) {
3940
const fixText = message.fix.text;
40-
41-
switch (message.ruleId) {
42-
case "@stylistic/quotes": {
43-
const quoteChar = fixText[0];
44-
const originalTextWithoutQuoteChars = originalText
45-
.slice(newRange[0] + 1, newRange[1] - 1);
46-
return `${quoteChar}${originalTextWithoutQuoteChars}${quoteChar}`;
47-
}
48-
default:
49-
return fixText;
50-
}
41+
return dummyReplacementsMap.apply(fixText);
5142
}
5243

5344
module.exports = { transformFix };

lib/cache.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ class Cache {
44
this.cache = new Map();
55
}
66

7-
add(filename, originalText, lintableText, offsetMap) {
8-
this.cache.set(filename, { originalText, lintableText, offsetMap });
7+
add(filename, originalText, lintableText, offsetMap, dummyReplacementsMap) {
8+
this.cache.set(filename, { originalText, lintableText, offsetMap, dummyReplacementsMap });
99
}
1010

1111
get(filename) {

lib/dummy_replacements_map.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Array that tracks the replacements that have been made in the processed text,
3+
* so that we can reverse them later.
4+
*/
5+
class DummyReplacementsMap {
6+
constructor() {
7+
this.replacements = [];
8+
}
9+
10+
addMapping(originalText, dummyText) {
11+
this.replacements.push([originalText, dummyText]);
12+
}
13+
14+
/**
15+
* Tries to apply all stored replacements to the given text.
16+
* @param {string} text - The text to apply the replacements to.
17+
* @returns {string} - The text with the replacements applied.
18+
*/
19+
apply(text) {
20+
let lintableText = text;
21+
for (const [originalText, dummyText] of this.replacements) {
22+
lintableText = lintableText.replace(dummyText, originalText);
23+
}
24+
return lintableText;
25+
}
26+
}
27+
28+
module.exports = {
29+
DummyReplacementsMap,
30+
};

lib/postprocess.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ const { transformFix } = require("./autofix.js");
1313
* @returns {Message[]} A flattened array of messages with mapped locations.
1414
*/
1515
function postprocess(messages, filename) {
16-
const { originalText, lintableText, offsetMap } = cache.get(filename);
16+
const { originalText, lintableText, offsetMap, dummyReplacementsMap } = cache.get(filename);
1717

1818
const transformedMessages = messages[0].map((message) => {
19-
return adjustMessage(message, originalText, lintableText, offsetMap);
19+
return adjustMessage(message, originalText, lintableText, offsetMap, dummyReplacementsMap);
2020
});
2121
cache.delete(filename);
2222

@@ -28,7 +28,7 @@ function postprocess(messages, filename) {
2828
* @param {Message} message message form ESLint
2929
* @returns {Message} same message, but adjusted to the correct location
3030
*/
31-
function adjustMessage(message, originalText, lintableText, offsetMap) {
31+
function adjustMessage(message, originalText, lintableText, offsetMap, dummyReplacementsMap) {
3232
if (!Number.isInteger(message.line)) {
3333
return {
3434
...message,
@@ -62,11 +62,10 @@ function adjustMessage(message, originalText, lintableText, offsetMap) {
6262
// Autofixes
6363
const adjustedFix = {};
6464
if (message.fix) {
65-
adjustedFix.fix = transformFix(message, originalText, lintableText, offsetMap);
65+
adjustedFix.fix = transformFix(message, originalText, lintableText,
66+
offsetMap, dummyReplacementsMap);
6667
}
6768

68-
// TODO: Implement suggestion ranges
69-
7069
return { ...message, ...newCoordinates, ...adjustedFix };
7170
}
7271

lib/preprocess.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ const { indexToColumn } = require("./file_coordinates.js");
77

88
// how annoying is that kind of import in JS ?!
99
var OffsetMap = require("./offset_map.js").OffsetMap;
10+
var DummyReplacementsMap = require("./dummy_replacements_map.js").DummyReplacementsMap;
1011

1112
const ERB_REGEX = /<%[\s\S]*?%>/g;
13+
const HASH = "566513c5d83ac26e15414f2754"; // to avoid collisions with user code
1214

1315
/**
1416
* Transforms the given text into lintable text. We do this by stripping out
@@ -17,8 +19,10 @@ const ERB_REGEX = /<%[\s\S]*?%>/g;
1719
* location of messages in the postprocess step later.
1820
* @param {string} text text of the file
1921
* @param {string} filename filename of the file
20-
* @param {string} dummyString dummy string to replace ERB tags with
21-
* (this is language-specific)
22+
* @param {(id) => string} dummyString dummy string to replace ERB tags with
23+
* (this is language-specific). Should be
24+
* a function that takes an id and returns
25+
* a UNIQUE dummy string.
2226
* @returns {Array<{ filename: string, text: string }>} source code blocks to lint
2327
*/
2428
function preprocess(text, filename, dummyString) {
@@ -28,7 +32,9 @@ function preprocess(text, filename, dummyString) {
2832
let numAddLines = 0;
2933
let numDiffChars = 0;
3034
const offsetMap = new OffsetMap();
35+
const dummyReplacementsMap = new DummyReplacementsMap();
3136

37+
let matchedId = 0;
3238
while ((match = ERB_REGEX.exec(text)) !== null) {
3339
// Match information
3440
const startIndex = match.index;
@@ -41,23 +47,28 @@ function preprocess(text, filename, dummyString) {
4147
numAddLines += matchLines.length - 1;
4248

4349
// Columns
50+
const dummy = dummyString(matchedId);
4451
const coordStartIndex = indexToColumn(text, startIndex);
45-
const endColumnAfter = coordStartIndex.column + dummyString.length;
52+
const endColumnAfter = coordStartIndex.column + dummy.length;
4653
const coordEndIndex = indexToColumn(text, endIndex);
4754
const endColumnBefore = coordEndIndex.column;
4855
const numAddColumns = endColumnBefore - endColumnAfter;
56+
replaceTextWithDummy(lintableTextArr, startIndex, matchLength - 1, dummy);
4957

50-
replaceTextWithDummy(lintableTextArr, startIndex, matchLength - 1, dummyString);
58+
const textWithErbSyntax = text.slice(startIndex, endIndex);
59+
dummyReplacementsMap.addMapping(textWithErbSyntax, dummy);
5160

5261
// Store in map
5362
const lineAfter = coordEndIndex.line - numAddLines;
54-
numDiffChars += dummyString.length - matchLength;
63+
numDiffChars += dummy.length - matchLength;
5564
const endIndexAfter = endIndex + numDiffChars;
5665
offsetMap.addMapping(endIndexAfter, lineAfter, numAddLines, numAddColumns);
66+
67+
matchedId += 1;
5768
}
5869

5970
const lintableText = lintableTextArr.join("");
60-
cache.add(filename, text, lintableText, offsetMap);
71+
cache.add(filename, text, lintableText, offsetMap, dummyReplacementsMap);
6172
return [lintableText];
6273
}
6374

@@ -69,20 +80,24 @@ function preprocess(text, filename, dummyString) {
6980
* For the length of the match, subsequent characters are replaced with empty
7081
* strings in the array.
7182
*/
72-
function replaceTextWithDummy(lintableTextArr, startIndex, length, dummyString) {
73-
lintableTextArr[startIndex] = dummyString;
83+
function replaceTextWithDummy(lintableTextArr, startIndex, length, dummy) {
84+
lintableTextArr[startIndex] = dummy;
7485
const replaceArgs = Array(length).join(".").split(".");
7586
// -> results in ['', '', '', '', ...]
7687
lintableTextArr.splice(startIndex + 1, length, ...replaceArgs);
7788
}
7889

7990
function preprocessJs(text, filename) {
80-
const dummyString = "/* eslint-disable */{}/* eslint-enable */";
91+
function dummyString(id) {
92+
return `/* eslint-disable */{}/* ${HASH} ${id} *//* eslint-enable */`;
93+
}
8194
return preprocess(text, filename, dummyString);
8295
}
8396

8497
function preprocessHtml(text, filename) {
85-
const dummyString = "<!-- -->";
98+
function dummyString(id) {
99+
return `<!-- ${HASH} ${id} -->`;
100+
}
86101
return preprocess(text, filename, dummyString);
87102
}
88103

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
"lib"
3232
],
3333
"devDependencies": {
34+
"@html-eslint/eslint-plugin": "^0.31.1",
35+
"@html-eslint/parser": "^0.31.0",
3436
"@stylistic/eslint-plugin": "^1.3.3",
3537
"chai": "^4.3.10",
36-
"eslint": "^9.0.0-alpha.0",
38+
"eslint": "^9.17.0",
3739
"globals": "^13.24.0",
3840
"mocha": "^10.2.0"
3941
},

tests/eslint.test.config.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const js = require("@eslint/js");
2+
const stylistic = require("@stylistic/eslint-plugin");
3+
const globals = require("globals");
4+
const html = require("@html-eslint/eslint-plugin");
5+
6+
const plugin = require("../lib/index.js");
7+
8+
const customizedStylistic = stylistic.configs.customize({
9+
"indent": 2,
10+
"jsx": false,
11+
"quote-props": "always",
12+
"semi": "always",
13+
"brace-style": "1tbs",
14+
});
15+
16+
module.exports = [
17+
js.configs.recommended,
18+
{
19+
processor: plugin.processors["processorJs"],
20+
files: ["**/*.js", "**/*.js.erb"],
21+
},
22+
{
23+
plugins: {
24+
"@stylistic": stylistic,
25+
},
26+
rules: {
27+
...customizedStylistic.rules,
28+
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
29+
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
30+
},
31+
languageOptions: {
32+
ecmaVersion: 2024,
33+
sourceType: "module",
34+
globals: {
35+
...globals.node,
36+
...globals.mocha,
37+
},
38+
},
39+
ignores: ["**/*.html**"],
40+
},
41+
{
42+
// HTML linting (aside from erb_lint)
43+
files: ["**/*.html", "**/*.html.erb"],
44+
processor: plugin.processors["processorHtml"],
45+
...html.configs["flat/recommended"],
46+
plugins: {
47+
"@html-eslint": html,
48+
"@stylistic": stylistic,
49+
},
50+
rules: {
51+
"@stylistic/eol-last": ["error", "always"],
52+
"@stylistic/no-trailing-spaces": "error",
53+
"@stylistic/no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0 }],
54+
...html.configs["flat/recommended"].rules,
55+
// 🎈 Best Practices
56+
"@html-eslint/no-extra-spacing-text": "error",
57+
"@html-eslint/no-script-style-type": "error",
58+
"@html-eslint/no-target-blank": "error",
59+
// 🎈 Accessibility
60+
"@html-eslint/no-abstract-roles": "error",
61+
"@html-eslint/no-accesskey-attrs": "error",
62+
"@html-eslint/no-aria-hidden-body": "error",
63+
"@html-eslint/no-non-scalable-viewport": "error",
64+
"@html-eslint/no-positive-tabindex": "error",
65+
"@html-eslint/no-skip-heading-levels": "error",
66+
// 🎈 Styles
67+
"@html-eslint/attrs-newline": ["error", {
68+
closeStyle: "newline",
69+
ifAttrsMoreThan: 5,
70+
}],
71+
"@html-eslint/id-naming-convention": ["error", "kebab-case"],
72+
"@html-eslint/indent": ["error", 2],
73+
"@html-eslint/sort-attrs": "error",
74+
"@html-eslint/no-extra-spacing-attrs": ["error", {
75+
enforceBeforeSelfClose: true,
76+
disallowMissing: true,
77+
disallowTabs: true,
78+
disallowInAssignment: true,
79+
}],
80+
},
81+
},
82+
];

tests/fixtures/common.expected.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
if (<%= @success %>) {
2+
console.log("Success 🎉");
3+
$("#yeah").html("<%= j render partial: 'yeah',
4+
locals: { cool: @cool } %>");
5+
}
6+
else {
7+
console.log("Sad noises");
8+
}
9+
10+
<% if you_feel_lucky %>
11+
console.log("You are lucky 🍀");
12+
<% end %>

0 commit comments

Comments
 (0)