Skip to content

Commit 7975e00

Browse files
authored
(fix) better fix range handling (#160)
Fixes don't need to start at the same line where the error message is shown, and they don't need to be at the same line as the start of the error message. Therefore the previous handling of re-adding dedents was flawed. Instead, map the range to positions (line/character) and use them to find the correct dedent offsets. Additionally add indentation after each newline a fix has because the fix doesn't know about the additional indentation. To properly test fixes, the test setup was enhanced. If there's a Fixed.svelte, that one will be compared against a fixed version of Input.svelte. Fixes #110 Fixes #156 Fixes #157
1 parent 9f33674 commit 7975e00

File tree

13 files changed

+278
-11
lines changed

13 files changed

+278
-11
lines changed

src/block.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ export const get_translation = (text, block, node, options = {}) => {
1313
block.transformed_code += '\n';
1414
const translation = { options, unoffsets: get_offsets(block.transformed_code) };
1515
translation.range = [node.start, node.end];
16-
const { dedented, offsets } = dedent_code(text.slice(node.start, node.end));
16+
const { dedented, offsets, indentation } = dedent_code(text.slice(node.start, node.end));
1717
block.transformed_code += dedented;
1818
translation.offsets = get_offsets(text.slice(0, node.start));
1919
translation.dedent = offsets;
20+
translation.indentation = indentation;
2021
translation.end = get_offsets(block.transformed_code).lines;
2122
for (let i = translation.unoffsets.lines; i <= translation.end; i++) {
2223
block.translations.set(i, translation);

src/mapping.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class DocumentMapper {
137137
* @param position Line and character position
138138
* @param text The text for which the offset should be retrieved
139139
*/
140-
function offset_at(position, text) {
140+
export function offset_at(position, text) {
141141
const line_offsets = get_line_offsets(text);
142142

143143
if (position.line >= line_offsets.length) {
@@ -153,7 +153,12 @@ function offset_at(position, text) {
153153
return clamp(next_line_offset, line_offset, line_offset + position.column);
154154
}
155155

156-
function position_at(offset, text) {
156+
/**
157+
* Get the line and character position of an offset
158+
* @param offset Idx of the offset
159+
* @param text The text for which the position should be retrieved
160+
*/
161+
export function position_at(offset, text) {
157162
offset = clamp(offset, 0, text.length);
158163

159164
const line_offsets = get_line_offsets(text);

src/postprocess.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { position_at } from './mapping.js';
12
import { state, reset } from './state.js';
23
import { get_line_offsets } from './utils.js';
34

45
// transform a linting message according to the module/instance script info we've gathered
5-
const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, range }, message) => {
6-
// strip out the start and end of the fix if they are not actually changes
6+
const transform_message = ({ transformed_code }, { unoffsets, dedent, indentation, offsets, range }, message) => {
7+
let fix_pos_start;
8+
let fix_pos_end;
79
if (message.fix) {
10+
// strip out the start and end of the fix if they are not actually changes
811
while (message.fix.range[0] < message.fix.range[1] && transformed_code[message.fix.range[0]] === message.fix.text[0]) {
912
message.fix.range[0]++;
1013
message.fix.text = message.fix.text.slice(1);
@@ -13,7 +16,15 @@ const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, r
1316
message.fix.range[1]--;
1417
message.fix.text = message.fix.text.slice(0, -1);
1518
}
19+
// If the fix spans multiple lines, add the indentation to each one
20+
message.fix.text = message.fix.text.split('\n').join(`\n${indentation}`);
21+
// The fix can span multiple lines and doesn't need to be on the same line
22+
// as the lint message, so we can't just add total_offsets at message.line to it
23+
// (see dedent-step below)
24+
fix_pos_start = position_at(message.fix.range[0], transformed_code);
25+
fix_pos_end = position_at(message.fix.range[1], transformed_code);
1626
}
27+
1728
// shift position reference backward according to unoffsets
1829
// (aka: throw out the generated code lines prior to the original code)
1930
{
@@ -33,6 +44,7 @@ const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, r
3344
message.fix.range[1] -= length;
3445
}
3546
}
47+
3648
// adjust position reference according to the previous dedenting
3749
// (aka: re-add the stripped indentation)
3850
{
@@ -42,10 +54,15 @@ const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, r
4254
message.endColumn += offsets[message.endLine - 1];
4355
}
4456
if (message.fix) {
45-
message.fix.range[0] += total_offsets[message.line];
46-
message.fix.range[1] += total_offsets[message.line];
57+
// We need the offset at the line relative to dedent's total_offsets. The dedents
58+
// start at the point in the total transformed code where a subset of the code was transformed.
59+
// Therefore substract (unoffsets.lines + 1) which marks the start of that transformation.
60+
// Add +1 afterwards because total_offsets are 1-index-based.
61+
message.fix.range[0] += total_offsets[fix_pos_start.line - unoffsets.lines + 2];
62+
message.fix.range[1] += total_offsets[fix_pos_end.line - unoffsets.lines + 2];
4763
}
4864
}
65+
4966
// shift position reference forward according to offsets
5067
// (aka: re-add the code that is originally prior to the code)
5168
{
@@ -65,6 +82,7 @@ const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, r
6582
message.fix.range[1] += length;
6683
}
6784
}
85+
6886
// make sure the fix doesn't include anything outside the range of the script
6987
if (message.fix) {
7088
if (message.fix.range[0] < range[0]) {

src/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const dedent_code = str => {
4646
}
4747
dedented += str[i];
4848
}
49-
return { dedented, offsets: { offsets, total_offsets } };
49+
return { dedented, offsets: { offsets, total_offsets }, indentation };
5050
};
5151

5252
// get character offsets of each line in a string

test/index.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
process.chdir(__dirname);
44

55
const { ESLint } = require('eslint');
6+
const { SourceCodeFixer } = require('eslint/lib/linter');
67
const assert = require('assert');
78
const fs = require('fs');
89

9-
const linter = new ESLint({ reportUnusedDisableDirectives: "error" });
10+
const linter = new ESLint({ reportUnusedDisableDirectives: 'error' });
1011

1112
async function run() {
1213
const tests = fs.readdirSync('samples').filter(name => name[0] !== '.');
@@ -15,24 +16,31 @@ async function run() {
1516
console.log(name);
1617

1718
const path_input = `samples/${name}/Input.svelte`;
19+
const path_fixed = `samples/${name}/Fixed.svelte`;
1820
const path_ple = `samples/${name}/preserve_line_endings`;
1921
const path_expected = `samples/${name}/expected.json`;
2022
const path_actual = `samples/${name}/actual.json`;
2123

2224
if (process.platform === 'win32' && !exists(path_ple)) {
23-
const file = fs.readFileSync(path_input, "utf-8");
25+
const file = fs.readFileSync(path_input, 'utf-8');
2426
fs.writeFileSync(path_input, file.replace(/\r/g, ''));
2527
}
2628

2729
const result = await linter.lintFiles(path_input);
2830

2931
const actual = result[0] ? result[0].messages : [];
30-
const expected = JSON.parse(fs.readFileSync(path_expected, "utf-8"));
32+
const expected = JSON.parse(fs.readFileSync(path_expected, 'utf-8'));
3133

3234
fs.writeFileSync(path_actual, JSON.stringify(actual, null, '\t'));
3335

3436
assert.equal(actual.length, expected.length);
3537
assert.deepStrictEqual(actual, actual.map((msg, i) => ({ ...msg, ...expected[i] })));
38+
39+
if (fs.existsSync(path_fixed)) {
40+
const fixed = SourceCodeFixer.applyFixes(fs.readFileSync(path_input, 'utf-8'), actual).output;
41+
assert.deepStrictEqual(fixed, fs.readFileSync(path_fixed, 'utf-8'))
42+
}
43+
3644
console.log('passed!\n');
3745
}
3846
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
rules: {
3+
curly: 'error',
4+
'no-else-return': 'error',
5+
'no-lonely-if': 'error',
6+
},
7+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
if (window.foo)
3+
{window.foo =
4+
window.bar
5+
.toLowerCase()
6+
.replace('something', 'else')
7+
.trim();}
8+
9+
function noElseReturn() {
10+
if (window) {
11+
return 'foo';
12+
}
13+
return 'bar';
14+
15+
}
16+
17+
function noLonelyIf() {
18+
if (window) {
19+
doX();
20+
} else if (!window) {
21+
doY();
22+
}
23+
}
24+
</script>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script>
2+
if (window.foo)
3+
window.foo =
4+
window.bar
5+
.toLowerCase()
6+
.replace('something', 'else')
7+
.trim();
8+
9+
function noElseReturn() {
10+
if (window) {
11+
return 'foo';
12+
} else {
13+
return 'bar';
14+
}
15+
}
16+
17+
function noLonelyIf() {
18+
if (window) {
19+
doX();
20+
} else {
21+
if (!window) {
22+
doY();
23+
}
24+
}
25+
}
26+
</script>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[
2+
{
3+
"ruleId": "curly",
4+
"severity": 2,
5+
"message": "Expected { after 'if' condition.",
6+
"line": 3,
7+
"column": 9,
8+
"nodeType": "IfStatement",
9+
"messageId": "missingCurlyAfterCondition",
10+
"endLine": 7,
11+
"endColumn": 25,
12+
"fix": {
13+
"range": [
14+
37,
15+
174
16+
],
17+
"text": "{window.foo =\n window.bar\n .toLowerCase()\n .replace('something', 'else')\n .trim();}"
18+
}
19+
},
20+
{
21+
"ruleId": "no-else-return",
22+
"severity": 2,
23+
"message": "Unnecessary 'else' after 'return'.",
24+
"line": 12,
25+
"column": 16,
26+
"nodeType": "BlockStatement",
27+
"messageId": "unexpected",
28+
"endLine": 14,
29+
"endColumn": 10,
30+
"fix": {
31+
"range": [
32+
264,
33+
306
34+
],
35+
"text": "\n return 'bar';\n "
36+
}
37+
},
38+
{
39+
"ruleId": "no-lonely-if",
40+
"severity": 2,
41+
"message": "Unexpected if as the only statement in an else block.",
42+
"line": 21,
43+
"column": 13,
44+
"nodeType": "IfStatement",
45+
"messageId": "unexpectedLonelyIf",
46+
"endLine": 23,
47+
"endColumn": 14,
48+
"fix": {
49+
"range": [
50+
398,
51+
468
52+
],
53+
"text": "if (!window) {\n doY();\n "
54+
}
55+
}
56+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = {
2+
parser: '@typescript-eslint/parser',
3+
plugins: ['@typescript-eslint'],
4+
settings: {
5+
'svelte3/typescript': require('typescript'),
6+
},
7+
rules: {
8+
curly: 'error',
9+
'no-else-return': 'error',
10+
'no-lonely-if': 'error',
11+
},
12+
};

0 commit comments

Comments
 (0)