Note to reviewer: There are two potential solutions here, one that fixes the specific issue linked, and one that might be a bit more robust. Filter down to the first commit for the bandaid fix, second commit for the more robust one, third commit for added test. I recommend hiding whitespace changes in the GitHub UI – my editor removed a bunch of unnecessary trailing spaces
PR Template
What:
Fixes https://github.com/less/less.js/issues/3567
Source maps are pointing to the wrong files, and it seems that it points at different files on different runs, causing non-deterministic source map output.
Checklist:
- [x] Documentation - N/A
- [x] Added/updated unit tests
- [x] Code complete
Why and How are included in the below description, which details the problem in the code and the proposed solutions.
Investigation and the problems
Bear with me a bit, this was my first venture into the less source code 🙂
We were seeing that we got weird source map output, mapping to files that shouldn't even appear in the source maps (e.g. reference imports from which nothing was used). After digging, there are a couple of problems that are causing this whole issue.
Background information
I was able to narrow down my search a bit by finding when this bug was introduced. It was released in 3.5.0-beta.2
, and the bug was introduced in https://github.com/less/less.js/pull/3227.
We were seeing this happen when a selector included a variable, which directed me to this bit: https://github.com/less/less.js/pull/3227/files#diff-d8c0204835f49ae90096efe1e2d0d80868e0e6214bfd4c960a097eb20cc14ec9R67-R83
It looks like what this is doing is recreating the selector CSS with the "variableCurly" node replaced with the variable's value. Then it parses that newly created CSS, and replaces the old Selector
node(s) with the new one(s). It looks like the code is trying to preserve source information by passing in the fileInfo
and index
of the original selectors to parseNode
: https://github.com/less/less.js/blob/eefe33a47f6fdcc228817df7435a1770ce9e51ea/packages/less/src/less/tree/ruleset.js#L85-L86
For a little more context, my understanding is that parseNode
is used for creating new nodes after the original input files are parsed.
First problem
In parseNode
, the provided currentIndex
and fileInfo
are added to the newly created node(s) here: https://github.com/less/less.js/blob/eefe33a47f6fdcc228817df7435a1770ce9e51ea/packages/less/src/less/parser/parser.js#L111-L112
These lines assume that result
is a tree node. However, in some cases, result
is actually an array of nodes. So when it tries to add the source info from the old selectors to the new ones, it's actually just setting _index
and _fileInfo
properties on the array itself, so it doesn't actually ever make it to the source maps. In this case, the parseList
is ["selectors"]
, so result
is an array of Selector
tree nodes.
Second problem
Selector
nodes have an elements
array, containing the elements that make up the selector. When a Selector
is added to the output, it actually does so by generating the CSS for each of its elements: https://github.com/less/less.js/blob/eefe33a47f6fdcc228817df7435a1770ce9e51ea/packages/less/src/less/tree/selector.js#L135-L138
This means that if the _fileInfo
or _index
for the Selector's elements is incorrect, then the output source map will be incorrect. That also means that even if the _fileInfo
and _index
were correctly set on the Selector
(s) rather than the array containing the Selector
(s), the source maps would still be incorrect on the Element
s that make up the selector. The source info for the elements gets set here: https://github.com/less/less.js/blob/eefe33a47f6fdcc228817df7435a1770ce9e51ea/packages/less/src/less/parser/parser.js#L1299
There are two problems here:
- The
currentIndex
passed into parseNode
doesn't get added to the created Element
s index
- That
fileInfo
is not the fileInfo
that gets passed into parseNode
. It's the fileInfo
that's given to the Parser
when it's instantiated: https://github.com/less/less.js/blob/eefe33a47f6fdcc228817df7435a1770ce9e51ea/packages/less/src/less/parser/parser.js#L41
The proposed solution(s)
I have two proposals for how we can solve this. One is more of a bandaid fix, and the other is (I believe) a bit more robust. Both solutions include the fix for where _fileInfo
and _index
get set on an array.
- Bandaid fix by going through the
elements
for each selector returned by parseNode
, and update the _fileInfo
and add the currentIndex
to the _index
for each one.
- Instead of calling
this.parse.parseNode
and relying on parseNode
to update the _index
and _fileInfo
of the created nodes, create a new Parser
object each time we want to call parseNode
. This removes the currentIndex
and fileInfo
params for parseNode
, and adds a currentIndex
param to the Parser
itself. All nodes created by the parser will add currentIndex
(which defaults to 0) to the index from parserInput
. This way, we can ensure that any nodes created with parseNode
will have the correct source information.
The first proposed solution can be seen by filtering to the first commit, and the second proposed solution can be seen by filtering to both the first and second commits. The third commit adds a test to ensure that selectors with variables in them have properly generated source maps. I recommend hiding whitespace changes in the GitHub UI – my editor removed a bunch of unnecessary trailing spaces.