Skip to content

Commit

Permalink
fix: node positional information for files with single-quotes in comm…
Browse files Browse the repository at this point in the history
…ents (#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
  • Loading branch information
nwalters512 authored Oct 9, 2021
1 parent c92f312 commit 4f36de8
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 4 deletions.
47 changes: 47 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,53 @@ module.exports = {

parser.parse();

// To handle double-slash comments (`//`) we end up creating a new tokenizer
// in certain cases (see `lib/nodes/inline-comment.js`). However, this means
// that any following node in the AST will have incorrect start/end positions
// on the `source` property. To fix that, we'll walk the AST and compute
// updated positions for all nodes.
parser.root.walk((node) => {
const offset = input.css.lastIndexOf(node.source.input.css);

if (offset === 0) {
// Short circuit - this node was processed with the original tokenizer
// and should therefore have correct position information.
return;
}

// This ensures that the chunk of source we're processing corresponds
// strictly to a terminal substring of the input CSS. This should always
// be the case, but if it ever isn't, we prefer to fail instead of
// producing potentially invalid output.
// istanbul ignore next
if (offset + node.source.input.css.length !== input.css.length) {
throw new Error('Invalid state detected in postcss-less');
}

const newStartOffset = offset + node.source.start.offset;
const newStartPosition = input.fromOffset(offset + node.source.start.offset);

// eslint-disable-next-line no-param-reassign
node.source.start = {
offset: newStartOffset,
line: newStartPosition.line,
column: newStartPosition.col
};

// Not all nodes have an `end` property.
if (node.source.end) {
const newEndOffset = offset + node.source.end.offset;
const newEndPosition = input.fromOffset(offset + node.source.end.offset);

// eslint-disable-next-line no-param-reassign
node.source.end = {
offset: newEndOffset,
line: newEndPosition.line,
column: newEndPosition.col
};
}
});

return parser.root;
},

Expand Down
14 changes: 10 additions & 4 deletions lib/nodes/inline-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
if (token[0] === 'word' && token[1].slice(0, 2) === '//') {
const first = token;
const bits = [];
let last;
let endOffset;
let remainingInput;

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

// Get remaining input and retokenize
remainingInput = token[1].substring(token[1].indexOf('\n'));
remainingInput += this.input.css.valueOf().substring(this.tokenizer.position());
const untokenizedRemainingInput = this.input.css
.valueOf()
.substring(this.tokenizer.position());
remainingInput += untokenizedRemainingInput;

endOffset = token[3] + untokenizedRemainingInput.length - remainingInput.length;
} else {
// If the tokenizer went to the next line go back
this.tokenizer.back(token);
Expand All @@ -29,11 +34,12 @@ module.exports = {
}

bits.push(token[1]);
last = token;
// eslint-disable-next-line prefer-destructuring
endOffset = token[2];
token = this.tokenizer.nextToken({ ignoreUnclosed: true });
}

const newToken = ['comment', bits.join(''), first[2], last[2]];
const newToken = ['comment', bits.join(''), first[2], endOffset];
this.inlineComment(newToken);

// Replace tokenizer to retokenize the rest of the string
Expand Down
33 changes: 33 additions & 0 deletions test/parser/comments.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,36 @@ test('inline comments with asterisk are persisted (#135)', (t) => {
t.is(first.text, '*batman');
t.is(nodeToString(root), less);
});

test('handles single quotes in comments (#163)', (t) => {
const less = `a {\n // '\n color: pink;\n}\n\n/** ' */`;

const root = parse(less);

const [ruleNode, commentNode] = root.nodes;

t.is(ruleNode.type, 'rule');
t.is(commentNode.type, 'comment');

t.is(commentNode.source.start.line, 6);
t.is(commentNode.source.start.column, 1);
t.is(commentNode.source.end.line, 6);
t.is(commentNode.source.end.column, 8);

const [innerCommentNode, declarationNode] = ruleNode.nodes;

t.is(innerCommentNode.type, 'comment');
t.is(declarationNode.type, 'decl');

t.is(innerCommentNode.source.start.line, 2);
t.is(innerCommentNode.source.start.column, 3);
t.is(innerCommentNode.source.end.line, 2);
t.is(innerCommentNode.source.end.column, 6);

t.is(declarationNode.source.start.line, 3);
t.is(declarationNode.source.start.column, 3);
t.is(declarationNode.source.end.line, 3);
t.is(declarationNode.source.end.column, 14);

t.is(nodeToString(root), less);
});

0 comments on commit 4f36de8

Please sign in to comment.