Skip to content

refactor: convert prototype-based tree nodes to ES6 classes#4412

Merged
matthew-dean merged 4 commits intoless:masterfrom
matthew-dean:feat/jsdoc-classes
Mar 10, 2026
Merged

refactor: convert prototype-based tree nodes to ES6 classes#4412
matthew-dean merged 4 commits intoless:masterfrom
matthew-dean:feat/jsdoc-classes

Conversation

@matthew-dean
Copy link
Member

@matthew-dean matthew-dean commented Mar 10, 2026

Summary

  • Convert all 30 tree node files from Foo.prototype = Object.assign(new Node(), {...}) to proper class Foo extends Node syntax
  • Node.type changed from instance property to getter, enabling clean subclass overrides via get type() { return 'Foo'; }
  • Factory functions in index.js updated from Object.create(t.prototype) + t.apply() to new t(...) (required for ES6 class compatibility)
  • Benchmark script converted from CJS to ESM

Inheritance chain

Class Extends
Most tree nodes Node
Ruleset Node
AtRule Node (+ NestableAtRulePrototype mixin)
MixinDefinition Ruleset
Media, Container AtRule (+ NestableAtRulePrototype mixin)
JsEvalNode Node
JavaScript JsEvalNode

Performance

No regression vs historical baselines (benchmark.less, 104KB):

Version Avg Render (ms) Throughput
v3.5.0–v3.9.0 36–40 ms 2592–2831 KB/s
This PR 36–39 ms 2654–2916 KB/s

Test plan

  • All 139 tests pass (grunt shell:test)
  • Benchmark shows no performance regression
  • tsc --noEmit passes with 0 errors (checkJs enabled)
  • Scanned for Object.create/.apply patterns that could break with class constructors

Summary by CodeRabbit

  • Refactoring

    • Modernized core code to ES6 class style for many internal components, improving consistency and maintainability.
  • Documentation

    • Added JSDoc/type annotations and parameter docs across modules for clearer developer-facing docs.
  • Bug Fixes / Reliability

    • Improved robustness of the style() handling to avoid parse failures when given invalid inputs.
    • Tweaked deprecation warning wording to indicate a replacement in upcoming v5.
  • Chores

    • Migrated benchmark tooling to ES Modules and added Node type defs to dev dependencies.

Convert all 30 tree node files from `Object.assign(new Node(), {...})`
prototype pattern to proper `class extends Node` syntax. This enables
TypeScript to understand the inheritance chain, reducing checkJs errors
from 2756 to 0.

- All tree nodes now use `class X extends Node` (or appropriate parent)
- Node.type converted from instance property to getter for clean override
- Factory functions in index.js updated to use `new` instead of
  Object.create + apply (required for ES6 class compatibility)
- Benchmark script converted to ESM
- Added @types/node devDependency for checkJs support
- Enabled checkJs in tsconfig.json
- Added JSDoc types to node.js base class and several utility files

No behavioral changes - all 139 tests pass, benchmark performance
unchanged vs historical baselines (avg 36-39ms for 104KB).
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Mar 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d4b358d3-0a9d-481a-98b2-b66638b8fcf2

📥 Commits

Reviewing files that changed from the base of the PR and between a906549 and 50992e7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • packages/less/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/less/package.json

📝 Walkthrough

Walkthrough

Large refactor converting ~40 tree node implementations from function+prototype to ES6 classes, expanding the Node base class with helpers and typedefs, adding JSDoc/type annotations across browser/node utilities and visitors, migrating the benchmark script to ESM, and a few wording/timing/exception handling tweaks. No public API signatures changed.

Changes

Cohort / File(s) Summary
Tree Node Class Conversions
packages/less/lib/less/tree/... (see files: anonymous.js, assignment.js, atrule.js, attribute.js, call.js, color.js, combinator.js, comment.js, condition.js, container.js, declaration.js, detached-ruleset.js, dimension.js, element.js, expression.js, extend.js, import.js, javascript.js, js-eval-node.js, keyword.js, media.js, mixin-call.js, mixin-definition.js, namespace-value.js, negative.js, operation.js, paren.js, property.js, query-in-parens.js, quoted.js, ruleset.js, selector.js, unicode-descriptor.js, unit.js, url.js, value.js, variable.js, variable-call.js)
Systematic migration from function+prototype implementations to ES6 class declarations extending Node; adds get type() accessors; methods moved into class bodies. Public behavior preserved; declarations changed across many node files.
Node Base Class
packages/less/lib/less/tree/node.js
Expanded with typedefs (FileInfo, VisibilityInfo, CSSOutput, EvalContext), new helpers (setParent, fileInfo, _operate, fround), visibility block management, revised compare/numericCompare and visibility APIs, additional accessors and utilities.
Browser & Node utilities (JSDoc/types)
packages/less/lib/less-browser/add-default-options.js, packages/less/lib/less-browser/utils.js, packages/less/lib/less-node/fs.js
Added JSDoc/type annotations and typedefs (including internal fs type); no behavioral changes.
Deprecation, style() and parser messages
packages/less/lib/less/deprecation.js, packages/less/lib/less/functions/style.js, packages/less/lib/less/parser/parser.js
Deprecation wording updated; style() now computes CSS via toCSS and is wrapped in try/catch returning undefined on error; parser @plugin warning text adjusted.
Visitors & Transform
packages/less/lib/less/visitors/set-tree-visibility-visitor.js, packages/less/lib/less/transform-tree.js
SetTreeVisibilityVisitor: added run, visitArray, visit methods and Node import; transform-tree received JSDoc only.
Benchmark & package metadata
packages/less/benchmark/index.js, packages/less/package.json
Benchmark script migrated from CommonJS to ESM (imports, fileURLToPath, __dirname derivation, let/const, performance.now()); added devDependency @types/node and reordered devDependencies.
Ctor helper
packages/less/lib/less/index.js
ctor wrapper simplified to use rest parameters and new t(...args) instead of manual prototypes/apply.
Misc. utilities
packages/less/lib/less/functions/style.js (local change)
style() now computes result via toCSS and returns undefined on exception to allow parser fallback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

size:XXL

Poem

🐰 Hopping through code where prototypes slept,
I planted classes where old patterns crept,
Nodes stand up straighter, helpers in a row,
A carrot for cleanup — quick, off we go!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: converting prototype-based tree nodes to ES6 classes, which is the central theme across most of the modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Mar 10, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/less/lib/less/tree/ruleset.js (1)

571-588: ⚠️ Potential issue | 🔴 Critical

Bug: createParenthesis accesses elementsToPak[0] when length is 0.

When elementsToPak.length === 0, the code creates new Paren(elementsToPak[0]) which passes undefined to Paren. The condition should be === 1 to handle the single-element case; otherwise iterate through the array to wrap each element.

🐛 Proposed fix
         function createParenthesis(elementsToPak, originalElement) {
             let replacementParen, j;
-            if (elementsToPak.length === 0) {
+            if (elementsToPak.length === 1) {
                 replacementParen = new Paren(elementsToPak[0]);
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/ruleset.js` around lines 571 - 588, The bug is in
createParenthesis: it uses elementsToPak[0] when elementsToPak.length === 0;
change the conditional to check for elementsToPak.length === 1 and only call new
Paren(elementsToPak[0]) in that case; leave the existing loop that builds
insideParent and new Paren(new Selector(insideParent)) for lengths > 1, ensuring
createParenthesis (and symbols Paren, Element, Selector, originalElement)
correctly handle the single-element and multi-element cases (and let the
empty-array case fall through to the multi-element branch or be handled
explicitly if needed).
🧹 Nitpick comments (8)
packages/less/lib/less/tree/variable.js (1)

14-55: Consider initializing evaluating property in constructor.

The evaluating property (used as a recursion guard at lines 21 and 28) is dynamically added but never declared in the constructor. While this works in JavaScript, explicitly initializing it would improve clarity and align with the pattern of declaring all instance properties in the constructor.

♻️ Optional: Declare evaluating in constructor
     constructor(name, index, currentFileInfo) {
         super();
         this.name = name;
         this._index = index;
         this._fileInfo = currentFileInfo;
+        this.evaluating = false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/variable.js` around lines 14 - 55, Add an
explicit initializing declaration for the evaluating flag in the Variable class
constructor: declare this.evaluating = false in the constructor (the same
constructor that sets this.name/this.index/fileInfo/etc.) so the recursion-guard
used in eval (references to this.evaluating at the start of eval and when
clearing it) is clearly defined on all instances.
packages/less/lib/less/tree/property.js (1)

60-65: Inconsistent property accessor usage.

Lines 63-64 use this.currentFileInfo.filename and this.index, while lines 23-24 use this.fileInfo().filename and this.getIndex(). Although they resolve to the same values (the properties are getters delegating to the methods), using a consistent style would improve readability.

♻️ Suggested consistency fix
         } else {
             throw { type: 'Name',
                 message: `Property '${name}' is undefined`,
-                filename: this.currentFileInfo.filename,
-                index: this.index };
+                filename: this.fileInfo().filename,
+                index: this.getIndex() };
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/property.js` around lines 60 - 65, In the thrown
error object in property.js, replace direct property access
this.currentFileInfo.filename and this.index with the consistent accessor calls
used elsewhere: this.fileInfo().filename and this.getIndex(); update the throw
block in the same method (the block that constructs the Name error) so it uses
these accessor methods to match other usages like fileInfo() and getIndex().
packages/less/lib/less/visitors/set-tree-visibility-visitor.js (1)

35-54: Consider using Array.isArray() instead of constructor comparison.

Line 39 uses node.constructor === Array which can fail for arrays from different realms (iframes, etc.). Using Array.isArray(node) is more robust and idiomatic.

💡 Suggested improvement
     visit(node) {
         if (!node) {
             return node;
         }
-        if (node.constructor === Array) {
+        if (Array.isArray(node)) {
             return this.visitArray(node);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/visitors/set-tree-visibility-visitor.js` around lines
35 - 54, In the visit method, replace the fragile constructor check
node.constructor === Array with a robust Array.isArray(node) check so arrays
from other realms are detected; update the condition that currently calls
this.visitArray(node) to use Array.isArray(node) instead, leaving the rest of
visit, visitArray, and subsequent behavior (ensureVisibility/ensureInvisibility
and node.accept(this)) unchanged.
packages/less/lib/less/tree/declaration.js (1)

101-107: Consider preserving the variable parameter in makeImportant.

The makeImportant method doesn't pass the variable parameter to the new Declaration, which means it will be re-derived from the name (Line 31-32). If the original declaration had an explicitly set variable value that differs from the name-based derivation, this could be lost.

This appears to be pre-existing behavior from before the class conversion, so it may be intentional. Verify if preserving this.variable is needed.

💡 Optional fix to preserve variable
     makeImportant() {
         return new Declaration(this.name,
             this.value,
             '!important',
             this.merge,
-            this.getIndex(), this.fileInfo(), this.inline);
+            this.getIndex(), this.fileInfo(), this.inline, this.variable);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/declaration.js` around lines 101 - 107, The
makeImportant() factory currently constructs a new Declaration without
preserving the original this.variable, so any explicitly-set variable flag is
lost; update makeImportant() to pass this.variable into the Declaration
constructor (preserving the original variable value) and ensure you place it at
the correct parameter position to match Declaration's signature (e.g., include
this.variable before/at the parameter spot where Declaration expects the
variable flag such as the variable/merge boundary) so the new Declaration
retains the same variable state as the original.
packages/less/lib/less/tree/unit.js (1)

51-53: Consider caching the RegExp for isLength.

Creating a new RegExp on every call is slightly inefficient. This is pre-existing behavior, but since you're refactoring this file, it could be a good opportunity to optimize.

💡 Optional optimization
+const LENGTH_UNITS_RE = /^(px|em|ex|ch|rem|in|cm|mm|pc|pt|ex|vw|vh|vmin|vmax)$/i;
+
 class Unit extends Node {
     // ... other code ...
 
     isLength() {
-        return RegExp('^(px|em|ex|ch|rem|in|cm|mm|pc|pt|ex|vw|vh|vmin|vmax)$', 'gi').test(this.toCSS());
+        return LENGTH_UNITS_RE.test(this.toCSS());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/unit.js` around lines 51 - 53, The isLength
method creates a new RegExp on every call; define a module-level cached RegExp
(e.g., const LENGTH_RE = /^(px|em|ex|ch|rem|in|cm|mm|pc|pt|vw|vh|vmin|vmax)$/i)
and replace the RegExp(...) construction in isLength with
LENGTH_RE.test(this.toCSS()); ensure you drop the global 'g' flag to avoid
stateful .test() behavior and reference the isLength method and this.toCSS()
when making the change.
packages/less/lib/less/tree/color.js (2)

32-38: Use forEach instead of map when not using return values.

Same issue as above—the callback only produces side effects.

♻️ Suggested fix
-        rgb.split('').map(function (c, i) {
+        rgb.split('').forEach(function (c, i) {
             if (i < 3) {
                 self.rgb.push(parseInt(c + c, 16));
             } else {
                 self.alpha = (parseInt(c + c, 16)) / 255;
             }
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/color.js` around lines 32 - 38, The callback
passed to rgb.split('').map in color.js produces only side effects, so replace
.map with .forEach to avoid creating an unused array; update the call on the
rgb.split('') result (within the constructor/parse logic that pushes into
self.rgb and sets self.alpha) to use .forEach with the same callback so behavior
is unchanged but no unnecessary array is allocated.

23-29: Use forEach instead of map when not using return values.

The callback doesn't return a value, making map() misleading. Since you're only using the side effect of populating this.rgb, forEach() is the correct method.

♻️ Suggested fix
-        rgb.match(/.{2}/g).map(function (c, i) {
+        rgb.match(/.{2}/g).forEach(function (c, i) {
             if (i < 3) {
                 self.rgb.push(parseInt(c, 16));
             } else {
                 self.alpha = (parseInt(c, 16)) / 255;
             }
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/color.js` around lines 23 - 29, Replace the use
of Array.prototype.map (which returns a new array) with Array.prototype.forEach
since the callback only performs side effects; specifically change the call on
rgb.match(/.{2}/g).map(...) to use .forEach(...) and keep the body that pushes
into self.rgb and sets self.alpha (referencing self.rgb and self.alpha)
unchanged.
packages/less/lib/less/tree/expression.js (1)

63-66: Operator precedence issue: missing parentheses in condition.

The || has lower precedence than instanceof and &&, so the current expression is parsed as:

(!(... instanceof Anonymous)) || ((... instanceof Anonymous) && (value !== ','))

This appears to be the intended logic (add space unless next element is an Anonymous with value ,), so the behavior is correct. However, the redundant second instanceof check could be simplified.

♻️ Suggested simplification
-            if (!(this.value[i + 1] instanceof Anonymous) ||
-                this.value[i + 1] instanceof Anonymous && this.value[i + 1].value !== ',') {
+            if (!(this.value[i + 1] instanceof Anonymous) || this.value[i + 1].value !== ',') {
                 output.add(' ');
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/expression.js` around lines 63 - 66, The
condition around output.add(' ') is redundant: instead of checking "if
(!(this.value[i+1] instanceof Anonymous) || this.value[i+1] instanceof Anonymous
&& this.value[i+1].value !== ',')" simplify to a single, clear test that adds a
space unless the next element is an Anonymous with value ','; replace it with a
single negated conjunction such as "if (!(this.value[i+1] instanceof Anonymous
&& this.value[i+1].value === ','))" referencing the same this.value and
Anonymous symbols so the logic is equivalent and easier to read.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/less/benchmark/index.js`:
- Line 9: The code uses path.join when overriding file with CLI input (the
expression handling process.argv[2] and assigning to file), which incorrectly
concatenates absolute paths; change that assignment to use path.resolve instead
of path.join so absolute paths are preserved and relative paths are resolved
against process.cwd(); update the branch that checks process.argv[2] and replace
path.join(...) with path.resolve(...) wherever the file override occurs (look
for the variable file and the use of path.join in that conditional).

In `@packages/less/lib/less/tree/atrule.js`:
- Around line 21-24: The code constructs a Selector with this._index and
this._fileInfo before those properties are initialized; move the assignments
that set this._index and this._fileInfo from the constructor parameters (index,
fileInfo) so they occur before the call to (new
Selector(...)).createEmptySelectors(), i.e., assign this._index = index;
this._fileInfo = fileInfo; (and any related property initializations) prior to
creating the empty selectors so Selector receives the correct values.

In `@packages/less/lib/less/tree/node.js`:
- Around line 190-206: The comparison logic in Node.compare uses aVal and bVal
but only checks that aVal is an array; add a defensive check that bVal is an
array too before reading bVal.length or indexing into bVal. In the Node.compare
implementation, after confirming Array.isArray(aVal) and before comparing
lengths or iterating, ensure Array.isArray(bVal) is true and return undefined if
not, so both arrays are guaranteed when using bVal.length and bVal[i].

In `@packages/less/package.json`:
- Line 75: The package.json currently pins "@types/node" to "~25.4.0" which
mismatches the declared Node support in "engines.node" (Node 18+); update the
dependency so typings align with the lowest supported Node major: change the
"@types/node" entry to a version compatible with Node 18 (e.g., "^18.x.x") or
alternatively tighten "engines.node" to require Node 25+, ensuring consistency
between the "engines.node" field and the "@types/node" dependency in
package.json.

---

Outside diff comments:
In `@packages/less/lib/less/tree/ruleset.js`:
- Around line 571-588: The bug is in createParenthesis: it uses elementsToPak[0]
when elementsToPak.length === 0; change the conditional to check for
elementsToPak.length === 1 and only call new Paren(elementsToPak[0]) in that
case; leave the existing loop that builds insideParent and new Paren(new
Selector(insideParent)) for lengths > 1, ensuring createParenthesis (and symbols
Paren, Element, Selector, originalElement) correctly handle the single-element
and multi-element cases (and let the empty-array case fall through to the
multi-element branch or be handled explicitly if needed).

---

Nitpick comments:
In `@packages/less/lib/less/tree/color.js`:
- Around line 32-38: The callback passed to rgb.split('').map in color.js
produces only side effects, so replace .map with .forEach to avoid creating an
unused array; update the call on the rgb.split('') result (within the
constructor/parse logic that pushes into self.rgb and sets self.alpha) to use
.forEach with the same callback so behavior is unchanged but no unnecessary
array is allocated.
- Around line 23-29: Replace the use of Array.prototype.map (which returns a new
array) with Array.prototype.forEach since the callback only performs side
effects; specifically change the call on rgb.match(/.{2}/g).map(...) to use
.forEach(...) and keep the body that pushes into self.rgb and sets self.alpha
(referencing self.rgb and self.alpha) unchanged.

In `@packages/less/lib/less/tree/declaration.js`:
- Around line 101-107: The makeImportant() factory currently constructs a new
Declaration without preserving the original this.variable, so any explicitly-set
variable flag is lost; update makeImportant() to pass this.variable into the
Declaration constructor (preserving the original variable value) and ensure you
place it at the correct parameter position to match Declaration's signature
(e.g., include this.variable before/at the parameter spot where Declaration
expects the variable flag such as the variable/merge boundary) so the new
Declaration retains the same variable state as the original.

In `@packages/less/lib/less/tree/expression.js`:
- Around line 63-66: The condition around output.add(' ') is redundant: instead
of checking "if (!(this.value[i+1] instanceof Anonymous) || this.value[i+1]
instanceof Anonymous && this.value[i+1].value !== ',')" simplify to a single,
clear test that adds a space unless the next element is an Anonymous with value
','; replace it with a single negated conjunction such as "if (!(this.value[i+1]
instanceof Anonymous && this.value[i+1].value === ','))" referencing the same
this.value and Anonymous symbols so the logic is equivalent and easier to read.

In `@packages/less/lib/less/tree/property.js`:
- Around line 60-65: In the thrown error object in property.js, replace direct
property access this.currentFileInfo.filename and this.index with the consistent
accessor calls used elsewhere: this.fileInfo().filename and this.getIndex();
update the throw block in the same method (the block that constructs the Name
error) so it uses these accessor methods to match other usages like fileInfo()
and getIndex().

In `@packages/less/lib/less/tree/unit.js`:
- Around line 51-53: The isLength method creates a new RegExp on every call;
define a module-level cached RegExp (e.g., const LENGTH_RE =
/^(px|em|ex|ch|rem|in|cm|mm|pc|pt|vw|vh|vmin|vmax)$/i) and replace the
RegExp(...) construction in isLength with LENGTH_RE.test(this.toCSS()); ensure
you drop the global 'g' flag to avoid stateful .test() behavior and reference
the isLength method and this.toCSS() when making the change.

In `@packages/less/lib/less/tree/variable.js`:
- Around line 14-55: Add an explicit initializing declaration for the evaluating
flag in the Variable class constructor: declare this.evaluating = false in the
constructor (the same constructor that sets this.name/this.index/fileInfo/etc.)
so the recursion-guard used in eval (references to this.evaluating at the start
of eval and when clearing it) is clearly defined on all instances.

In `@packages/less/lib/less/visitors/set-tree-visibility-visitor.js`:
- Around line 35-54: In the visit method, replace the fragile constructor check
node.constructor === Array with a robust Array.isArray(node) check so arrays
from other realms are detected; update the condition that currently calls
this.visitArray(node) to use Array.isArray(node) instead, leaving the rest of
visit, visitArray, and subsequent behavior (ensureVisibility/ensureInvisibility
and node.accept(this)) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4da9e184-2ac1-4781-b468-d711b31662ca

📥 Commits

Reviewing files that changed from the base of the PR and between c21e465 and 0e3003b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (49)
  • packages/less/benchmark/index.js
  • packages/less/lib/less-browser/add-default-options.js
  • packages/less/lib/less-browser/utils.js
  • packages/less/lib/less-node/fs.js
  • packages/less/lib/less/deprecation.js
  • packages/less/lib/less/functions/style.js
  • packages/less/lib/less/index.js
  • packages/less/lib/less/transform-tree.js
  • packages/less/lib/less/tree/anonymous.js
  • packages/less/lib/less/tree/assignment.js
  • packages/less/lib/less/tree/atrule.js
  • packages/less/lib/less/tree/attribute.js
  • packages/less/lib/less/tree/call.js
  • packages/less/lib/less/tree/color.js
  • packages/less/lib/less/tree/combinator.js
  • packages/less/lib/less/tree/comment.js
  • packages/less/lib/less/tree/condition.js
  • packages/less/lib/less/tree/container.js
  • packages/less/lib/less/tree/declaration.js
  • packages/less/lib/less/tree/detached-ruleset.js
  • packages/less/lib/less/tree/dimension.js
  • packages/less/lib/less/tree/element.js
  • packages/less/lib/less/tree/expression.js
  • packages/less/lib/less/tree/extend.js
  • packages/less/lib/less/tree/import.js
  • packages/less/lib/less/tree/javascript.js
  • packages/less/lib/less/tree/js-eval-node.js
  • packages/less/lib/less/tree/keyword.js
  • packages/less/lib/less/tree/media.js
  • packages/less/lib/less/tree/mixin-call.js
  • packages/less/lib/less/tree/mixin-definition.js
  • packages/less/lib/less/tree/namespace-value.js
  • packages/less/lib/less/tree/negative.js
  • packages/less/lib/less/tree/node.js
  • packages/less/lib/less/tree/operation.js
  • packages/less/lib/less/tree/paren.js
  • packages/less/lib/less/tree/property.js
  • packages/less/lib/less/tree/query-in-parens.js
  • packages/less/lib/less/tree/quoted.js
  • packages/less/lib/less/tree/ruleset.js
  • packages/less/lib/less/tree/selector.js
  • packages/less/lib/less/tree/unicode-descriptor.js
  • packages/less/lib/less/tree/unit.js
  • packages/less/lib/less/tree/url.js
  • packages/less/lib/less/tree/value.js
  • packages/less/lib/less/tree/variable-call.js
  • packages/less/lib/less/tree/variable.js
  • packages/less/lib/less/visitors/set-tree-visibility-visitor.js
  • packages/less/package.json

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/less/lib/less/tree/atrule.js (1)

140-152: ⚠️ Potential issue | 🔴 Critical

Guard empty blocks before touching rules[0].

outputRuleset() already treats [] as valid, but eval() does not. An empty simple block such as @font-face {} leaves rules === []; Line 141 passes that into evalRoot(), and Line 205 dereferences rules[0]. Even if that path is skipped, Line 152 makes the same assumption. Add a zero-length guard so empty at-rules can round-trip instead of throwing.

🐛 Proposed fix
-        if (rules) {
+        if (rules && rules.length > 0) {
             rules = this.evalRoot(context, rules);
         }
@@
-        if (this.simpleBlock && rules) {
+        if (this.simpleBlock && rules && rules.length > 0) {
             rules[0].functionRegistry = context.frames[0].functionRegistry.inherit();
             rules = rules.map(function (rule) { return rule.eval(context); });
         }
@@
     evalRoot(context, rules) {
+        if (!rules.length) {
+            return rules;
+        }
+
         let ampersandCount = 0;
         let noAmpersandCount = 0;
         let noAmpersands = true;

Also applies to: 162-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/atrule.js` around lines 140 - 152, The code
assumes rules[0] exists when handling at-rules, which breaks for empty simple
blocks (rules === []); update the checks in the eval logic (around evalRoot
usage and the branches using declarationsBlock, mergeRules, and simpleBlock) to
guard against zero-length arrays before dereferencing rules[0] (e.g., ensure
Array.isArray(rules) && rules.length > 0 before accessing rules[0].rules or
setting rules[0].functionRegistry); apply the same zero-length guard to the
other similar block in the 162-207 region so empty at-rules like `@font-face` {}
are preserved instead of throwing.
🧹 Nitpick comments (1)
packages/less/lib/less/tree/atrule.js (1)

22-24: Avoid allocating and parenting a throwaway selector list.

The selector array created at Line 23 is never attached to the instance. In the non-array branch, Line 53 creates a second empty selector list, while Line 61 only calls setParent() on the detached first one. This adds extra work on a hot path and makes the ownership of the real selectors harder to follow. Create the empty selectors lazily in the branch that needs them and reuse that same reference.

♻️ Possible cleanup
-        let i;
-        var selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
+        let i;
+        let selectors;
@@
-                    this.rules = [rules];
-                    this.rules[0].selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
+                    this.rules = [rules];
+                    selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
+                    this.rules[0].selectors = selectors;
@@
-            this.setParent(selectors, this);
+            if (selectors) {
+                this.setParent(selectors, this);
+            }
             this.setParent(this.rules, this);

Also applies to: 53-53, 61-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/less/lib/less/tree/atrule.js` around lines 22 - 24, The code
currently always allocates a throwaway Selector list via new
Selector(...).createEmptySelectors() assigned to the local selectors var but
only needs it in the non-array branch; instead, create the empty selector list
lazily inside the branch that requires it and reuse that same reference for both
attaching and calling setParent(), removing the early allocation and the
separate second creation at the non-array branch. Update the logic around the
selectors variable, Selector.createEmptySelectors() usage, and the setParent()
call so the same selector list instance is created once when needed and then
passed/parented where required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/less/benchmark/index.js`:
- Around line 9-13: The fs.readFile callback uses data.length without checking
the error parameter (e), so a failed read will cause a TypeError and hide the
real I/O error; update the fs.readFile callback in
packages/less/benchmark/index.js (the function handling fs.readFile(file,
'utf8', function (e, data) {...})) to first check if e is truthy and log or
throw the actual error (and exit/return) before accessing data or data.length,
ensuring the real fs error is surfaced instead of dereferencing undefined.

---

Outside diff comments:
In `@packages/less/lib/less/tree/atrule.js`:
- Around line 140-152: The code assumes rules[0] exists when handling at-rules,
which breaks for empty simple blocks (rules === []); update the checks in the
eval logic (around evalRoot usage and the branches using declarationsBlock,
mergeRules, and simpleBlock) to guard against zero-length arrays before
dereferencing rules[0] (e.g., ensure Array.isArray(rules) && rules.length > 0
before accessing rules[0].rules or setting rules[0].functionRegistry); apply the
same zero-length guard to the other similar block in the 162-207 region so empty
at-rules like `@font-face` {} are preserved instead of throwing.

---

Nitpick comments:
In `@packages/less/lib/less/tree/atrule.js`:
- Around line 22-24: The code currently always allocates a throwaway Selector
list via new Selector(...).createEmptySelectors() assigned to the local
selectors var but only needs it in the non-array branch; instead, create the
empty selector list lazily inside the branch that requires it and reuse that
same reference for both attaching and calling setParent(), removing the early
allocation and the separate second creation at the non-array branch. Update the
logic around the selectors variable, Selector.createEmptySelectors() usage, and
the setParent() call so the same selector list instance is created once when
needed and then passed/parented where required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2e7cd3de-d287-47f3-a1d0-4bec32bdebba

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3003b and a906549.

📒 Files selected for processing (4)
  • packages/less/benchmark/index.js
  • packages/less/lib/less/deprecation.js
  • packages/less/lib/less/parser/parser.js
  • packages/less/lib/less/tree/atrule.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/less/lib/less/deprecation.js

@matthew-dean matthew-dean merged commit 76fc00c into less:master Mar 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant