Skip to content

Commit 4f36de8

Browse files
authored
fix: node positional information for files with single-quotes in comments (#164)
* Add test to reproduce error * Enhance test * Drop .only * Work on post-processing tree * Enhance code + tests * Add additional test assertions * Fix computation of end offset of comment * Add istanbul ignore comment
1 parent c92f312 commit 4f36de8

File tree

3 files changed

+90
-4
lines changed

3 files changed

+90
-4
lines changed

lib/index.js

+47
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,53 @@ module.exports = {
1212

1313
parser.parse();
1414

15+
// To handle double-slash comments (`//`) we end up creating a new tokenizer
16+
// in certain cases (see `lib/nodes/inline-comment.js`). However, this means
17+
// that any following node in the AST will have incorrect start/end positions
18+
// on the `source` property. To fix that, we'll walk the AST and compute
19+
// updated positions for all nodes.
20+
parser.root.walk((node) => {
21+
const offset = input.css.lastIndexOf(node.source.input.css);
22+
23+
if (offset === 0) {
24+
// Short circuit - this node was processed with the original tokenizer
25+
// and should therefore have correct position information.
26+
return;
27+
}
28+
29+
// This ensures that the chunk of source we're processing corresponds
30+
// strictly to a terminal substring of the input CSS. This should always
31+
// be the case, but if it ever isn't, we prefer to fail instead of
32+
// producing potentially invalid output.
33+
// istanbul ignore next
34+
if (offset + node.source.input.css.length !== input.css.length) {
35+
throw new Error('Invalid state detected in postcss-less');
36+
}
37+
38+
const newStartOffset = offset + node.source.start.offset;
39+
const newStartPosition = input.fromOffset(offset + node.source.start.offset);
40+
41+
// eslint-disable-next-line no-param-reassign
42+
node.source.start = {
43+
offset: newStartOffset,
44+
line: newStartPosition.line,
45+
column: newStartPosition.col
46+
};
47+
48+
// Not all nodes have an `end` property.
49+
if (node.source.end) {
50+
const newEndOffset = offset + node.source.end.offset;
51+
const newEndPosition = input.fromOffset(offset + node.source.end.offset);
52+
53+
// eslint-disable-next-line no-param-reassign
54+
node.source.end = {
55+
offset: newEndOffset,
56+
line: newEndPosition.line,
57+
column: newEndPosition.col
58+
};
59+
}
60+
});
61+
1562
return parser.root;
1663
},
1764

lib/nodes/inline-comment.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = {
88
if (token[0] === 'word' && token[1].slice(0, 2) === '//') {
99
const first = token;
1010
const bits = [];
11-
let last;
11+
let endOffset;
1212
let remainingInput;
1313

1414
while (token) {
@@ -20,7 +20,12 @@ module.exports = {
2020

2121
// Get remaining input and retokenize
2222
remainingInput = token[1].substring(token[1].indexOf('\n'));
23-
remainingInput += this.input.css.valueOf().substring(this.tokenizer.position());
23+
const untokenizedRemainingInput = this.input.css
24+
.valueOf()
25+
.substring(this.tokenizer.position());
26+
remainingInput += untokenizedRemainingInput;
27+
28+
endOffset = token[3] + untokenizedRemainingInput.length - remainingInput.length;
2429
} else {
2530
// If the tokenizer went to the next line go back
2631
this.tokenizer.back(token);
@@ -29,11 +34,12 @@ module.exports = {
2934
}
3035

3136
bits.push(token[1]);
32-
last = token;
37+
// eslint-disable-next-line prefer-destructuring
38+
endOffset = token[2];
3339
token = this.tokenizer.nextToken({ ignoreUnclosed: true });
3440
}
3541

36-
const newToken = ['comment', bits.join(''), first[2], last[2]];
42+
const newToken = ['comment', bits.join(''), first[2], endOffset];
3743
this.inlineComment(newToken);
3844

3945
// Replace tokenizer to retokenize the rest of the string

test/parser/comments.test.js

+33
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,36 @@ test('inline comments with asterisk are persisted (#135)', (t) => {
179179
t.is(first.text, '*batman');
180180
t.is(nodeToString(root), less);
181181
});
182+
183+
test('handles single quotes in comments (#163)', (t) => {
184+
const less = `a {\n // '\n color: pink;\n}\n\n/** ' */`;
185+
186+
const root = parse(less);
187+
188+
const [ruleNode, commentNode] = root.nodes;
189+
190+
t.is(ruleNode.type, 'rule');
191+
t.is(commentNode.type, 'comment');
192+
193+
t.is(commentNode.source.start.line, 6);
194+
t.is(commentNode.source.start.column, 1);
195+
t.is(commentNode.source.end.line, 6);
196+
t.is(commentNode.source.end.column, 8);
197+
198+
const [innerCommentNode, declarationNode] = ruleNode.nodes;
199+
200+
t.is(innerCommentNode.type, 'comment');
201+
t.is(declarationNode.type, 'decl');
202+
203+
t.is(innerCommentNode.source.start.line, 2);
204+
t.is(innerCommentNode.source.start.column, 3);
205+
t.is(innerCommentNode.source.end.line, 2);
206+
t.is(innerCommentNode.source.end.column, 6);
207+
208+
t.is(declarationNode.source.start.line, 3);
209+
t.is(declarationNode.source.start.column, 3);
210+
t.is(declarationNode.source.end.line, 3);
211+
t.is(declarationNode.source.end.column, 14);
212+
213+
t.is(nodeToString(root), less);
214+
});

0 commit comments

Comments
 (0)