-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Context:
rangeBywas introduced in feat: Support ranges for errors and warnings #1669, specifically to aid Stylelint.positionBywas introduced in 31f3e86, specifically to aid Stylelint (related issue: Add options for granular positioning to warn and error functions #348)positionAtwas introduced in 8d5d1b8, specifically to aid Stylelint (related issue: Add options for granular positioning to warn and error functions #348)positionAtwas renamed to the currentpositionInside
Each method that calculates these positions (positionInside, positionBy, rangeBy, ...) starts by taking the source start position:
Line 191 in 1cc6ac3
| let column = this.source.start.column |
This is the position of the current node as found in the original source, not the current position of serializing the entire AST.
However each function uses this.toString() to scan for new lines or to find the index of a given word:
Line 190 in 1cc6ac3
| let string = stringRepresentation || this.toString() |
This leads to inconsistencies and bugs when making mutations to the AST.
A minimal reproduction:
a {
one: 1;
two: 2;
}a.positionInside(15)->{ column: 3, line: 3 }two.positionInside(1)->{ column: 3, line: 3 }
If we remove one: 1; we alter the string representation of a { ... }, but not that of two: 2;.
If we then calculate the same positions we get different and inconsistent outcomes:
a.positionInside(15)->{ column: 12, line: 2 }two.positionInside(1)->{ column: 3, line: 3 }
a.positionInside(15) reflects the position after mutation.
While two.positionInside(1) reflects the position in the original source.
I found that any positions calculated without making doing mutations are reliable and usable. But once you start changing or removing AST nodes these functions become unpredictable.
test('positionInside() returns original source position even after remove the previous node', () => {
let css = parse('a {\n\tone: 1;\n\ttwo: 2;}')
let a = css.first as Rule
let one = a.first as Declaration
let two = one.next() as Declaration
equal(a.positionInside(15), { column: 3, line: 3 })
equal(two.positionInside(1), { column: 3, line: 3 })
one.remove()
equal(a.positionInside(15), { column: 3, line: 3 }) // fails, actual value is `{ column: 12, line: 2 }`
equal(two.positionInside(1), { column: 3, line: 3 })
})I encountered this issue while working on Stylelint and found that using this.source.input.css instead of this.toString() gives the expected result.
I hope that a fix can be considered here because these API's were added with Stylelint in mind and the current behavior is inconsistent.
Happy to provide a PR with a proposed fix.