From d0eb4307a9a39309b9a3236f20df68df09a94b5d Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Tue, 10 Mar 2026 09:03:24 -0700 Subject: [PATCH 1/2] feat: add JSDoc type annotations with @ts-check to all tree node files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add proper JSDoc type annotations to all 44 files in lib/less/tree/, enabling per-file TypeScript checking via @ts-check. No {*} or {any} casts — all types are derived from reading the actual code. Key changes: - Shared types (EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo) defined in node.js - Node.value typed as union: Node | Node[] | string | number | undefined - Node.prototype.parse declared for parser-injected prototype property - Constructor properties explicitly declared with proper types - Inline casts used to narrow union types at usage sites - Widened base class params where subclasses pass different types Also adds typecheck to prepublishOnly and pre-commit hook to catch regressions as more files are annotated toward global checkJs: true. All 139 tests pass, zero TypeScript errors. --- .husky/pre-commit | 1 + packages/less/lib/less/tree/anonymous.js | 26 +- packages/less/lib/less/tree/assignment.js | 27 +- packages/less/lib/less/tree/atrule-syntax.js | 1 + packages/less/lib/less/tree/atrule.js | 119 ++++-- packages/less/lib/less/tree/attribute.js | 28 +- packages/less/lib/less/tree/call.js | 28 +- packages/less/lib/less/tree/color.js | 59 ++- packages/less/lib/less/tree/combinator.js | 11 +- packages/less/lib/less/tree/comment.js | 25 +- packages/less/lib/less/tree/condition.js | 56 ++- packages/less/lib/less/tree/container.js | 54 ++- packages/less/lib/less/tree/debug-info.js | 30 +- packages/less/lib/less/tree/declaration.js | 59 ++- .../less/lib/less/tree/detached-ruleset.js | 15 + packages/less/lib/less/tree/dimension.js | 60 ++- packages/less/lib/less/tree/element.js | 39 +- packages/less/lib/less/tree/expression.js | 48 ++- packages/less/lib/less/tree/extend.js | 28 +- packages/less/lib/less/tree/import.js | 133 +++++-- packages/less/lib/less/tree/index.js | 7 +- packages/less/lib/less/tree/javascript.js | 22 +- packages/less/lib/less/tree/js-eval-node.js | 30 +- packages/less/lib/less/tree/keyword.js | 10 +- packages/less/lib/less/tree/media.js | 46 ++- packages/less/lib/less/tree/merge-rules.js | 10 +- packages/less/lib/less/tree/mixin-call.js | 112 ++++-- .../less/lib/less/tree/mixin-definition.js | 124 +++++-- .../less/lib/less/tree/namespace-value.js | 43 ++- packages/less/lib/less/tree/negative.js | 19 +- packages/less/lib/less/tree/nested-at-rule.js | 132 +++++-- packages/less/lib/less/tree/node.js | 68 +++- packages/less/lib/less/tree/operation.js | 29 +- packages/less/lib/less/tree/paren.js | 17 +- packages/less/lib/less/tree/property.js | 25 +- .../less/lib/less/tree/query-in-parens.js | 19 +- packages/less/lib/less/tree/quoted.js | 66 +++- packages/less/lib/less/tree/ruleset.js | 343 ++++++++++++++---- packages/less/lib/less/tree/selector.js | 62 +++- .../less/lib/less/tree/unicode-descriptor.js | 2 + packages/less/lib/less/tree/unit.js | 32 +- packages/less/lib/less/tree/url.js | 41 ++- packages/less/lib/less/tree/value.js | 28 +- packages/less/lib/less/tree/variable-call.js | 26 +- packages/less/lib/less/tree/variable.js | 23 +- packages/less/package.json | 4 +- 46 files changed, 1725 insertions(+), 462 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 98475b507..e06089e7e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ +cd packages/less && npm run typecheck && cd ../.. pnpm test diff --git a/packages/less/lib/less/tree/anonymous.js b/packages/less/lib/less/tree/anonymous.js index 977b15638..f87134e54 100644 --- a/packages/less/lib/less/tree/anonymous.js +++ b/packages/less/lib/less/tree/anonymous.js @@ -1,8 +1,18 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */ import Node from './node.js'; class Anonymous extends Node { get type() { return 'Anonymous'; } + /** + * @param {string | null} value + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {boolean} [mapLines] + * @param {boolean} [rulesetLike] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(value, index, currentFileInfo, mapLines, rulesetLike, visibilityInfo) { super(); this.value = value; @@ -14,22 +24,32 @@ class Anonymous extends Node { this.copyVisibilityInfo(visibilityInfo); } + /** @returns {Anonymous} */ eval() { - return new Anonymous(this.value, this._index, this._fileInfo, this.mapLines, this.rulesetLike, this.visibilityInfo()); + return new Anonymous(/** @type {string | null} */ (this.value), this._index, this._fileInfo, this.mapLines, this.rulesetLike, this.visibilityInfo()); } + /** + * @param {Node} other + * @returns {number | undefined} + */ compare(other) { - return other.toCSS && this.toCSS() === other.toCSS() ? 0 : undefined; + return other.toCSS && this.toCSS(/** @type {EvalContext} */ ({})) === other.toCSS(/** @type {EvalContext} */ ({})) ? 0 : undefined; } + /** @returns {boolean} */ isRulesetLike() { return this.rulesetLike; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { this.nodeVisible = Boolean(this.value); if (this.nodeVisible) { - output.add(this.value, this._fileInfo, this._index, this.mapLines); + output.add(/** @type {string} */ (this.value), this._fileInfo, this._index, this.mapLines); } } } diff --git a/packages/less/lib/less/tree/assignment.js b/packages/less/lib/less/tree/assignment.js index c53e58c4c..38acb331f 100644 --- a/packages/less/lib/less/tree/assignment.js +++ b/packages/less/lib/less/tree/assignment.js @@ -1,31 +1,46 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */ import Node from './node.js'; class Assignment extends Node { get type() { return 'Assignment'; } + /** + * @param {string} key + * @param {Node} val + */ constructor(key, val) { super(); this.key = key; this.value = val; } + /** @param {TreeVisitor} visitor */ accept(visitor) { - this.value = visitor.visit(this.value); + this.value = visitor.visit(/** @type {Node} */ (this.value)); } + /** + * @param {EvalContext} context + * @returns {Assignment} + */ eval(context) { - if (this.value.eval) { - return new Assignment(this.key, this.value.eval(context)); + if (/** @type {Node} */ (this.value).eval) { + return new Assignment(this.key, /** @type {Node} */ (this.value).eval(context)); } return this; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add(`${this.key}=`); - if (this.value.genCSS) { - this.value.genCSS(context, output); + if (/** @type {Node} */ (this.value).genCSS) { + /** @type {Node} */ (this.value).genCSS(context, output); } else { - output.add(this.value); + output.add(/** @type {string} */ (/** @type {unknown} */ (this.value))); } } } diff --git a/packages/less/lib/less/tree/atrule-syntax.js b/packages/less/lib/less/tree/atrule-syntax.js index 0c5decb83..fae273b41 100644 --- a/packages/less/lib/less/tree/atrule-syntax.js +++ b/packages/less/lib/less/tree/atrule-syntax.js @@ -1,3 +1,4 @@ +// @ts-check export const MediaSyntaxOptions = { queryInParens: true }; diff --git a/packages/less/lib/less/tree/atrule.js b/packages/less/lib/less/tree/atrule.js index 4f5343c0a..80787ad65 100644 --- a/packages/less/lib/less/tree/atrule.js +++ b/packages/less/lib/less/tree/atrule.js @@ -1,3 +1,6 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */ +/** @import { FunctionRegistry } from './nested-at-rule.js' */ import Node from './node.js'; import Selector from './selector.js'; import Ruleset from './ruleset.js'; @@ -5,9 +8,32 @@ import Anonymous from './anonymous.js'; import NestableAtRulePrototype from './nested-at-rule.js'; import mergeRules from './merge-rules.js'; +/** + * @typedef {Node & { + * rules?: Node[], + * selectors?: Selector[], + * root?: boolean, + * allowImports?: boolean, + * functionRegistry?: FunctionRegistry, + * merge?: boolean, + * debugInfo?: { lineNumber: number, fileName: string }, + * elements?: import('./element.js').default[] + * }} RulesetLikeNode + */ + class AtRule extends Node { get type() { return 'AtRule'; } + /** + * @param {string} [name] + * @param {Node | string} [value] + * @param {Node[] | Ruleset} [rules] + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {{ lineNumber: number, fileName: string }} [debugInfo] + * @param {boolean} [isRooted] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor( name, value, @@ -22,15 +48,22 @@ class AtRule extends Node { let i; var selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors(); + /** @type {string | undefined} */ this.name = name; this.value = (value instanceof Node) ? value : (value ? new Anonymous(value) : value); + /** @type {boolean | undefined} */ + this.simpleBlock = undefined; + /** @type {RulesetLikeNode[] | undefined} */ + this.declarations = undefined; + /** @type {RulesetLikeNode[] | undefined} */ + this.rules = undefined; if (rules) { if (Array.isArray(rules)) { const allDeclarations = this.declarationsBlock(rules); let allRulesetDeclarations = true; rules.forEach(rule => { - if (rule.type === 'Ruleset' && rule.rules) allRulesetDeclarations = allRulesetDeclarations && this.declarationsBlock(rule.rules, true); + if (rule.type === 'Ruleset' && /** @type {RulesetLikeNode} */ (rule).rules) allRulesetDeclarations = allRulesetDeclarations && this.declarationsBlock(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rule).rules), true); }); if (allDeclarations && !isRooted) { @@ -38,53 +71,65 @@ class AtRule extends Node { this.declarations = rules; } else if (allRulesetDeclarations && rules.length === 1 && !isRooted && !value) { this.simpleBlock = true; - this.declarations = rules[0].rules ? rules[0].rules : rules; + this.declarations = /** @type {RulesetLikeNode} */ (rules[0]).rules ? /** @type {RulesetLikeNode} */ (rules[0]).rules : rules; } else { this.rules = rules; } } else { - const allDeclarations = this.declarationsBlock(rules.rules); + const allDeclarations = this.declarationsBlock(/** @type {Node[]} */ (rules.rules)); if (allDeclarations && !isRooted && !value) { this.simpleBlock = true; this.declarations = rules.rules; } else { this.rules = [rules]; - this.rules[0].selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors(); + /** @type {RulesetLikeNode} */ (this.rules[0]).selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors(); } } if (!this.simpleBlock) { for (i = 0; i < this.rules.length; i++) { - this.rules[i].allowImports = true; + /** @type {RulesetLikeNode} */ (this.rules[i]).allowImports = true; } } - this.setParent(selectors, this); - this.setParent(this.rules, this); + this.setParent(selectors, /** @type {Node} */ (/** @type {unknown} */ (this))); + this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this))); } this._index = index; this._fileInfo = currentFileInfo; + /** @type {{ lineNumber: number, fileName: string } | undefined} */ this.debugInfo = debugInfo; + /** @type {boolean} */ this.isRooted = isRooted || false; this.copyVisibilityInfo(visibilityInfo); this.allowRoot = true; } + /** + * @param {Node[]} rules + * @param {boolean} [mergeable] + * @returns {boolean} + */ declarationsBlock(rules, mergeable = false) { if (!mergeable) { - return rules.filter(function (node) { return (node.type === 'Declaration' || node.type === 'Comment') && !node.merge}).length === rules.length; + return rules.filter(function (/** @type {Node & { merge?: boolean }} */ node) { return (node.type === 'Declaration' || node.type === 'Comment') && !node.merge}).length === rules.length; } else { - return rules.filter(function (node) { return (node.type === 'Declaration' || node.type === 'Comment'); }).length === rules.length; + return rules.filter(function (/** @type {Node} */ node) { return (node.type === 'Declaration' || node.type === 'Comment'); }).length === rules.length; } } + /** + * @param {Node[]} rules + * @returns {boolean} + */ keywordList(rules) { if (!Array.isArray(rules)) { return false; } else { - return rules.filter(function (node) { return (node.type === 'Keyword' || node.type === 'Comment'); }).length === rules.length; + return rules.filter(function (/** @type {Node} */ node) { return (node.type === 'Keyword' || node.type === 'Comment'); }).length === rules.length; } } + /** @param {TreeVisitor} visitor */ accept(visitor) { const value = this.value, rules = this.rules, declarations = this.declarations; @@ -94,27 +139,32 @@ class AtRule extends Node { this.declarations = visitor.visitArray(declarations); } if (value) { - this.value = visitor.visit(value); + this.value = visitor.visit(/** @type {Node} */ (value)); } } + /** @override @returns {boolean} */ isRulesetLike() { - return this.rules || !this.isCharset(); + return /** @type {boolean} */ (/** @type {unknown} */ (this.rules || !this.isCharset())); } isCharset() { return '@charset' === this.name; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { const value = this.value, rules = this.rules || this.declarations; - output.add(this.name, this.fileInfo(), this.getIndex()); + output.add(/** @type {string} */ (this.name), this.fileInfo(), this.getIndex()); if (value) { output.add(' '); - value.genCSS(context, output); + /** @type {Node} */ (value).genCSS(context, output); } if (this.simpleBlock) { - this.outputRuleset(context, output, this.declarations); + this.outputRuleset(context, output, /** @type {Node[]} */ (this.declarations)); } else if (rules) { this.outputRuleset(context, output, rules); } else { @@ -122,6 +172,10 @@ class AtRule extends Node { } } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { let mediaPathBackup, mediaBlocksBackup, value = this.value, rules = this.rules || this.declarations; @@ -134,31 +188,36 @@ class AtRule extends Node { context.mediaBlocks = []; if (value) { - value = value.eval(context); + value = /** @type {Node} */ (value).eval(context); } if (rules) { rules = this.evalRoot(context, rules); } - if (Array.isArray(rules) && rules[0].rules && Array.isArray(rules[0].rules) && rules[0].rules.length) { - const allMergeableDeclarations = this.declarationsBlock(rules[0].rules, true); + if (Array.isArray(rules) && /** @type {RulesetLikeNode} */ (rules[0]).rules && Array.isArray(/** @type {RulesetLikeNode} */ (rules[0]).rules) && /** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules).length) { + const allMergeableDeclarations = this.declarationsBlock(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules), true); if (allMergeableDeclarations && !this.isRooted && !value) { - mergeRules(rules[0].rules); - rules = rules[0].rules; - rules.forEach(rule => rule.merge = false); + mergeRules(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules)); + rules = /** @type {RulesetLikeNode[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules); + rules.forEach(/** @param {RulesetLikeNode} rule */ rule => { rule.merge = false; }); } } if (this.simpleBlock && rules) { - rules[0].functionRegistry = context.frames[0].functionRegistry.inherit(); - rules = rules.map(function (rule) { return rule.eval(context); }); + /** @type {RulesetLikeNode} */ (rules[0]).functionRegistry = /** @type {RulesetLikeNode} */ (context.frames[0]).functionRegistry.inherit(); + rules = rules.map(function (/** @type {Node} */ rule) { return rule.eval(context); }); } // restore media bubbling information context.mediaPath = mediaPathBackup; context.mediaBlocks = mediaBlocksBackup; - return new AtRule(this.name, value, rules, this.getIndex(), this.fileInfo(), this.debugInfo, this.isRooted, this.visibilityInfo()); + return /** @type {Node} */ (/** @type {unknown} */ (new AtRule(this.name, value, rules, this.getIndex(), this.fileInfo(), this.debugInfo, this.isRooted, this.visibilityInfo()))); } + /** + * @param {EvalContext} context + * @param {Node[]} rules + * @returns {Node[]} + */ evalRoot(context, rules) { let ampersandCount = 0; let noAmpersandCount = 0; @@ -168,10 +227,11 @@ class AtRule extends Node { rules = [rules[0].eval(context)]; } + /** @type {Selector[]} */ let precedingSelectors = []; if (context.frames.length > 0) { for (let index = 0; index < context.frames.length; index++) { - const frame = context.frames[index]; + const frame = /** @type {RulesetLikeNode} */ (context.frames[index]); if ( frame.type === 'Ruleset' && frame.rules && @@ -184,6 +244,7 @@ class AtRule extends Node { if (precedingSelectors.length > 0) { const allAmpersandElements = precedingSelectors.every( sel => sel.elements && sel.elements.length > 0 && sel.elements.every( + /** @param {import('./element.js').default} el */ el => el.value === '&' ) ); @@ -202,11 +263,12 @@ class AtRule extends Node { (this.isRooted && ampersandCount > 0 && noAmpersandCount === 0 && noAmpersands) || !mixedAmpersands ) { - rules[0].root = true; + /** @type {RulesetLikeNode} */ (rules[0]).root = true; } return rules; } + /** @param {string} name */ variable(name) { if (this.rules) { // assuming that there is only one rule at this point - that is how parser constructs the rule @@ -228,6 +290,11 @@ class AtRule extends Node { } } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + * @param {Node[]} rules + */ outputRuleset(context, output, rules) { const ruleCnt = rules.length; let i; diff --git a/packages/less/lib/less/tree/attribute.js b/packages/less/lib/less/tree/attribute.js index da3ea2006..52dbcebbf 100644 --- a/packages/less/lib/less/tree/attribute.js +++ b/packages/less/lib/less/tree/attribute.js @@ -1,8 +1,16 @@ +// @ts-check +/** @import { EvalContext, CSSOutput } from './node.js' */ import Node from './node.js'; class Attribute extends Node { get type() { return 'Attribute'; } + /** + * @param {string | Node} key + * @param {string} op + * @param {string | Node} value + * @param {string} cif + */ constructor(key, op, value, cif) { super(); this.key = key; @@ -11,25 +19,37 @@ class Attribute extends Node { this.cif = cif; } + /** + * @param {EvalContext} context + * @returns {Attribute} + */ eval(context) { return new Attribute( - this.key.eval ? this.key.eval(context) : this.key, + /** @type {Node} */ (this.key).eval ? /** @type {Node} */ (this.key).eval(context) : /** @type {string} */ (this.key), this.op, - (this.value && this.value.eval) ? this.value.eval(context) : this.value, + (this.value && /** @type {Node} */ (this.value).eval) ? /** @type {Node} */ (this.value).eval(context) : this.value, this.cif ); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add(this.toCSS(context)); } + /** + * @param {EvalContext} context + * @returns {string} + */ toCSS(context) { - let value = this.key.toCSS ? this.key.toCSS(context) : this.key; + let value = /** @type {Node} */ (this.key).toCSS ? /** @type {Node} */ (this.key).toCSS(context) : /** @type {string} */ (this.key); if (this.op) { value += this.op; - value += (this.value.toCSS ? this.value.toCSS(context) : this.value); + value += (/** @type {Node} */ (this.value).toCSS ? /** @type {Node} */ (this.value).toCSS(context) : /** @type {string} */ (this.value)); } if (this.cif) { diff --git a/packages/less/lib/less/tree/call.js b/packages/less/lib/less/tree/call.js index 1f54afbda..ceaac8746 100644 --- a/packages/less/lib/less/tree/call.js +++ b/packages/less/lib/less/tree/call.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo } from './node.js' */ import Node from './node.js'; import Anonymous from './anonymous.js'; import FunctionCaller from '../functions/function-caller.js'; @@ -8,6 +10,12 @@ import FunctionCaller from '../functions/function-caller.js'; class Call extends Node { get type() { return 'Call'; } + /** + * @param {string} name + * @param {Node[]} args + * @param {number} index + * @param {FileInfo} currentFileInfo + */ constructor(name, args, index, currentFileInfo) { super(); this.name = name; @@ -17,6 +25,7 @@ class Call extends Node { this._fileInfo = currentFileInfo; } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.args) { this.args = visitor.visitArray(this.args); @@ -34,6 +43,10 @@ class Call extends Node { // we try to pass a variable to a function, like: `saturate(@color)`. // The function should receive the value, not the variable. // + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { /** * Turn off math for calc(), and switch back on for evaluating nested functions @@ -51,6 +64,7 @@ class Call extends Node { context.mathOn = currentMathContext; }; + /** @type {Node | string | boolean | null | undefined} */ let result; const funcCaller = new FunctionCaller(this.name, context, this.getIndex(), this.fileInfo()); @@ -60,16 +74,16 @@ class Call extends Node { exitCalc(); } catch (e) { // eslint-disable-next-line no-prototype-builtins - if (e.hasOwnProperty('line') && e.hasOwnProperty('column')) { + if (/** @type {Record} */ (e).hasOwnProperty('line') && /** @type {Record} */ (e).hasOwnProperty('column')) { throw e; } throw { - type: e.type || 'Runtime', - message: `Error evaluating function \`${this.name}\`${e.message ? `: ${e.message}` : ''}`, + type: /** @type {Record} */ (e).type || 'Runtime', + message: `Error evaluating function \`${this.name}\`${/** @type {Error} */ (e).message ? `: ${/** @type {Error} */ (e).message}` : ''}`, index: this.getIndex(), filename: this.fileInfo().filename, - line: e.lineNumber, - column: e.columnNumber + line: /** @type {Record} */ (e).lineNumber, + column: /** @type {Record} */ (e).columnNumber }; } } @@ -97,6 +111,10 @@ class Call extends Node { return new Call(this.name, args, this.getIndex(), this.fileInfo()); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add(`${this.name}(`, this.fileInfo(), this.getIndex()); diff --git a/packages/less/lib/less/tree/color.js b/packages/less/lib/less/tree/color.js index 6b66b7543..bf5f1c687 100644 --- a/packages/less/lib/less/tree/color.js +++ b/packages/less/lib/less/tree/color.js @@ -1,12 +1,20 @@ +// @ts-check import Node from './node.js'; import colors from '../data/colors.js'; +/** @import { EvalContext, CSSOutput } from './node.js' */ + // // RGB Colors - #ff0014, #eee // class Color extends Node { get type() { return 'Color'; } + /** + * @param {number[] | string} rgb + * @param {number} [a] + * @param {string} [originalForm] + */ constructor(rgb, a, originalForm) { super(); const self = this; @@ -17,10 +25,12 @@ class Color extends Node { // This facilitates operations and conversions. // if (Array.isArray(rgb)) { + /** @type {number[]} */ this.rgb = rgb; } else if (rgb.length >= 6) { + /** @type {number[]} */ this.rgb = []; - rgb.match(/.{2}/g).map(function (c, i) { + /** @type {RegExpMatchArray} */ (rgb.match(/.{2}/g)).map(function (c, i) { if (i < 3) { self.rgb.push(parseInt(c, 16)); } else { @@ -28,6 +38,7 @@ class Color extends Node { } }); } else { + /** @type {number[]} */ this.rgb = []; rgb.split('').map(function (c, i) { if (i < 3) { @@ -37,6 +48,7 @@ class Color extends Node { } }); } + /** @type {number} */ this.alpha = this.alpha || (typeof a === 'number' ? a : 1); if (typeof originalForm !== 'undefined') { this.value = originalForm; @@ -53,15 +65,26 @@ class Color extends Node { return 0.2126 * r + 0.7152 * g + 0.0722 * b; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add(this.toCSS(context)); } + /** + * @param {EvalContext} context + * @param {boolean} [doNotCompress] + * @returns {string} + */ toCSS(context, doNotCompress) { const compress = context && context.compress && !doNotCompress; let color; let alpha; + /** @type {string | undefined} */ let colorFunction; + /** @type {(string | number)[]} */ let args = []; // `value` is set if this color was originally @@ -70,18 +93,18 @@ class Color extends Node { alpha = this.fround(context, this.alpha); if (this.value) { - if (this.value.indexOf('rgb') === 0) { + if (/** @type {string} */ (this.value).indexOf('rgb') === 0) { if (alpha < 1) { colorFunction = 'rgba'; } - } else if (this.value.indexOf('hsl') === 0) { + } else if (/** @type {string} */ (this.value).indexOf('hsl') === 0) { if (alpha < 1) { colorFunction = 'hsla'; } else { colorFunction = 'hsl'; } } else { - return this.value; + return /** @type {string} */ (this.value); } } else { if (alpha < 1) { @@ -132,6 +155,11 @@ class Color extends Node { // our result, in the form of an integer triplet, // we create a new Color node to hold the result. // + /** + * @param {EvalContext} context + * @param {string} op + * @param {Color} other + */ operate(context, op, other) { const rgb = new Array(3); const alpha = this.alpha * (1 - other.alpha) + other.alpha; @@ -149,6 +177,7 @@ class Color extends Node { const r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255, a = this.alpha; const max = Math.max(r, g, b), min = Math.min(r, g, b); + /** @type {number} */ let h; let s; const l = (max + min) / 2; @@ -164,9 +193,9 @@ class Color extends Node { case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } - h /= 6; + /** @type {number} */ (h) /= 6; } - return { h: h * 360, s, l, a }; + return { h: /** @type {number} */ (h) * 360, s, l, a }; } // Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript @@ -174,6 +203,7 @@ class Color extends Node { const r = this.rgb[0] / 255, g = this.rgb[1] / 255, b = this.rgb[2] / 255, a = this.alpha; const max = Math.max(r, g, b), min = Math.min(r, g, b); + /** @type {number} */ let h; let s; const v = max; @@ -193,15 +223,19 @@ class Color extends Node { case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } - h /= 6; + /** @type {number} */ (h) /= 6; } - return { h: h * 360, s, v, a }; + return { h: /** @type {number} */ (h) * 360, s, v, a }; } toARGB() { return toHex([this.alpha * 255].concat(this.rgb)); } + /** + * @param {Node & { rgb?: number[], alpha?: number }} x + * @returns {0 | undefined} + */ compare(x) { return (x.rgb && x.rgb[0] === this.rgb[0] && @@ -210,12 +244,14 @@ class Color extends Node { x.alpha === this.alpha) ? 0 : undefined; } + /** @param {string} keyword */ static fromKeyword(keyword) { + /** @type {Color | undefined} */ let c; const key = keyword.toLowerCase(); // eslint-disable-next-line no-prototype-builtins if (colors.hasOwnProperty(key)) { - c = new Color(colors[key].slice(1)); + c = new Color(/** @type {string} */ (colors[/** @type {keyof typeof colors} */ (key)]).slice(1)); } else if (key === 'transparent') { c = new Color([0, 0, 0], 0); @@ -228,10 +264,15 @@ class Color extends Node { } } +/** + * @param {number} v + * @param {number} max + */ function clamp(v, max) { return Math.min(Math.max(v, 0), max); } +/** @param {number[]} v */ function toHex(v) { return `#${v.map(function (c) { c = clamp(Math.round(c), 255); diff --git a/packages/less/lib/less/tree/combinator.js b/packages/less/lib/less/tree/combinator.js index 03b39b41a..b171c553a 100644 --- a/packages/less/lib/less/tree/combinator.js +++ b/packages/less/lib/less/tree/combinator.js @@ -1,5 +1,9 @@ +// @ts-check import Node from './node.js'; +/** @import { EvalContext, CSSOutput } from './node.js' */ + +/** @type {Record} */ const _noSpaceCombinators = { '': true, ' ': true, @@ -9,6 +13,7 @@ const _noSpaceCombinators = { class Combinator extends Node { get type() { return 'Combinator'; } + /** @param {string} value */ constructor(value) { super(); if (value === ' ') { @@ -20,8 +25,12 @@ class Combinator extends Node { } } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { - const spaceOrEmpty = (context.compress || _noSpaceCombinators[this.value]) ? '' : ' '; + const spaceOrEmpty = (context.compress || _noSpaceCombinators[/** @type {string} */ (this.value)]) ? '' : ' '; output.add(spaceOrEmpty + this.value + spaceOrEmpty); } } diff --git a/packages/less/lib/less/tree/comment.js b/packages/less/lib/less/tree/comment.js index 730904c81..d99850bb9 100644 --- a/packages/less/lib/less/tree/comment.js +++ b/packages/less/lib/less/tree/comment.js @@ -1,9 +1,18 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, FileInfo } from './node.js' */ +/** @import { DebugInfoContext } from './debug-info.js' */ import Node from './node.js'; import getDebugInfo from './debug-info.js'; class Comment extends Node { get type() { return 'Comment'; } + /** + * @param {string} value + * @param {boolean} isLineComment + * @param {number} index + * @param {FileInfo} currentFileInfo + */ constructor(value, isLineComment, index, currentFileInfo) { super(); this.value = value; @@ -11,17 +20,27 @@ class Comment extends Node { this._index = index; this._fileInfo = currentFileInfo; this.allowRoot = true; + /** @type {{ lineNumber: number, fileName: string } | undefined} */ + this.debugInfo = undefined; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { if (this.debugInfo) { - output.add(getDebugInfo(context, this), this.fileInfo(), this.getIndex()); + output.add(getDebugInfo(context, /** @type {DebugInfoContext} */ (this)), this.fileInfo(), this.getIndex()); } - output.add(this.value); + output.add(/** @type {string} */ (this.value)); } + /** + * @param {EvalContext} context + * @returns {boolean} + */ isSilent(context) { - const isCompressed = context.compress && this.value[2] !== '!'; + const isCompressed = context.compress && /** @type {string} */ (this.value)[2] !== '!'; return this.isLineComment || isCompressed; } } diff --git a/packages/less/lib/less/tree/condition.js b/packages/less/lib/less/tree/condition.js index 69eb351a7..0637a3eb9 100644 --- a/packages/less/lib/less/tree/condition.js +++ b/packages/less/lib/less/tree/condition.js @@ -1,8 +1,17 @@ +// @ts-check +/** @import { EvalContext, TreeVisitor } from './node.js' */ import Node from './node.js'; class Condition extends Node { get type() { return 'Condition'; } + /** + * @param {string} op + * @param {Node} l + * @param {Node} r + * @param {number} i + * @param {boolean} negate + */ constructor(op, l, r, i, negate) { super(); this.op = op.trim(); @@ -12,29 +21,42 @@ class Condition extends Node { this.negate = negate; } + /** @param {TreeVisitor} visitor */ accept(visitor) { this.lvalue = visitor.visit(this.lvalue); this.rvalue = visitor.visit(this.rvalue); } + /** + * @param {EvalContext} context + * @returns {boolean} + * @suppress {checkTypes} + */ + // @ts-ignore - Condition.eval returns boolean, not Node (used as guard condition) eval(context) { - const result = (function (op, a, b) { - switch (op) { - case 'and': return a && b; - case 'or': return a || b; - default: - switch (Node.compare(a, b)) { - case -1: - return op === '<' || op === '=<' || op === '<='; - case 0: - return op === '=' || op === '>=' || op === '=<' || op === '<='; - case 1: - return op === '>' || op === '>='; - default: - return false; - } - } - })(this.op, this.lvalue.eval(context), this.rvalue.eval(context)); + const a = this.lvalue.eval(context); + const b = this.rvalue.eval(context); + /** @type {boolean} */ + let result; + + switch (this.op) { + case 'and': result = Boolean(a && b); break; + case 'or': result = Boolean(a || b); break; + default: + switch (Node.compare(a, b)) { + case -1: + result = this.op === '<' || this.op === '=<' || this.op === '<='; + break; + case 0: + result = this.op === '=' || this.op === '>=' || this.op === '=<' || this.op === '<='; + break; + case 1: + result = this.op === '>' || this.op === '>='; + break; + default: + result = false; + } + } return this.negate ? !result : result; } diff --git a/packages/less/lib/less/tree/container.js b/packages/less/lib/less/tree/container.js index 9e19bea4a..3d317ba87 100644 --- a/packages/less/lib/less/tree/container.js +++ b/packages/less/lib/less/tree/container.js @@ -1,12 +1,31 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */ +/** @import { FunctionRegistry, NestableAtRuleThis } from './nested-at-rule.js' */ +import Node from './node.js'; import Ruleset from './ruleset.js'; import Value from './value.js'; import Selector from './selector.js'; import AtRule from './atrule.js'; import NestableAtRulePrototype from './nested-at-rule.js'; +/** + * @typedef {Ruleset & { + * allowImports?: boolean, + * debugInfo?: { lineNumber: number, fileName: string }, + * functionRegistry?: FunctionRegistry + * }} RulesetWithExtras + */ + class Container extends AtRule { get type() { return 'Container'; } + /** + * @param {Node[] | null} value + * @param {Node[]} features + * @param {number} index + * @param {FileInfo} currentFileInfo + * @param {VisibilityInfo} visibilityInfo + */ constructor(value, features, index, currentFileInfo, visibilityInfo) { super(); this._index = index; @@ -14,25 +33,38 @@ class Container extends AtRule { const selectors = (new Selector([], null, null, this._index, this._fileInfo)).createEmptySelectors(); + /** @type {Value} */ this.features = new Value(features); + /** @type {RulesetWithExtras[]} */ this.rules = [new Ruleset(selectors, value)]; this.rules[0].allowImports = true; this.copyVisibilityInfo(visibilityInfo); this.allowRoot = true; - this.setParent(selectors, this); - this.setParent(this.features, this); - this.setParent(this.rules, this); + this.setParent(selectors, /** @type {Node} */ (/** @type {unknown} */ (this))); + this.setParent(this.features, /** @type {Node} */ (/** @type {unknown} */ (this))); + this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this))); + + /** @type {boolean | undefined} */ + this._evaluated = undefined; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add('@container ', this._fileInfo, this._index); this.features.genCSS(context, output); this.outputRuleset(context, output, this.rules); } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { if (this._evaluated) { - return this; + return /** @type {Node} */ (/** @type {unknown} */ (this)); } if (!context.mediaBlocks) { context.mediaBlocks = []; @@ -46,20 +78,20 @@ class Container extends AtRule { media.debugInfo = this.debugInfo; } - media.features = this.features.eval(context); + media.features = /** @type {Value} */ (this.features.eval(context)); - context.mediaPath.push(media); - context.mediaBlocks.push(media); + context.mediaPath.push(/** @type {Node} */ (/** @type {unknown} */ (media))); + context.mediaBlocks.push(/** @type {Node} */ (/** @type {unknown} */ (media))); - this.rules[0].functionRegistry = context.frames[0].functionRegistry.inherit(); + this.rules[0].functionRegistry = /** @type {RulesetWithExtras} */ (context.frames[0]).functionRegistry.inherit(); context.frames.unshift(this.rules[0]); - media.rules = [this.rules[0].eval(context)]; + media.rules = [/** @type {RulesetWithExtras} */ (this.rules[0].eval(context))]; context.frames.shift(); context.mediaPath.pop(); - return context.mediaPath.length === 0 ? media.evalTop(context) : - media.evalNested(context); + return context.mediaPath.length === 0 ? /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalTop(context) : + /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalNested(context); } } diff --git a/packages/less/lib/less/tree/debug-info.js b/packages/less/lib/less/tree/debug-info.js index b8224e577..6c2c34fee 100644 --- a/packages/less/lib/less/tree/debug-info.js +++ b/packages/less/lib/less/tree/debug-info.js @@ -1,8 +1,21 @@ +// @ts-check + +/** + * @typedef {object} DebugInfoData + * @property {number} lineNumber + * @property {string} fileName + */ + +/** + * @typedef {object} DebugInfoContext + * @property {DebugInfoData} debugInfo + */ + /** * @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. * This will be removed in a future version. - * - * @param {Object} ctx - Context object with debugInfo + * + * @param {DebugInfoContext} ctx - Context object with debugInfo * @returns {string} Debug info as CSS comment */ function asComment(ctx) { @@ -14,8 +27,8 @@ function asComment(ctx) { * This function generates Sass-compatible debug info using @media -sass-debug-info syntax. * This format had short-lived usage and is no longer recommended. * This will be removed in a future version. - * - * @param {Object} ctx - Context object with debugInfo + * + * @param {DebugInfoContext} ctx - Context object with debugInfo * @returns {string} Sass-compatible debug info as @media query */ function asMediaQuery(ctx) { @@ -33,12 +46,12 @@ function asMediaQuery(ctx) { /** * Generates debug information (line numbers) for CSS output. - * - * @param {Object} context - Context object with dumpLineNumbers option - * @param {Object} ctx - Context object with debugInfo + * + * @param {{ dumpLineNumbers?: string, compress?: boolean }} context - Context object with dumpLineNumbers option + * @param {DebugInfoContext} ctx - Context object with debugInfo * @param {string} [lineSeparator] - Separator between comment and media query (for 'all' mode) * @returns {string} Debug info string - * + * * @deprecated The dumpLineNumbers option is deprecated. Use sourcemaps instead. * All modes ('comments', 'mediaquery', 'all') are deprecated and will be removed in a future version. * The 'mediaquery' and 'all' modes generate Sass-compatible @media -sass-debug-info output @@ -63,4 +76,3 @@ function debugInfo(context, ctx, lineSeparator) { } export default debugInfo; - diff --git a/packages/less/lib/less/tree/declaration.js b/packages/less/lib/less/tree/declaration.js index 37552d5e8..caff4d56a 100644 --- a/packages/less/lib/less/tree/declaration.js +++ b/packages/less/lib/less/tree/declaration.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, FileInfo } from './node.js' */ import Node from './node.js'; import Value from './value.js'; import Keyword from './keyword.js'; @@ -5,11 +7,17 @@ import Anonymous from './anonymous.js'; import * as Constants from '../constants.js'; const MATH = Constants.Math; +/** + * @param {EvalContext} context + * @param {Node[]} name + * @returns {string} + */ function evalName(context, name) { let value = ''; let i; const n = name.length; - const output = {add: function (s) {value += s;}}; + /** @type {CSSOutput} */ + const output = {add: function (s) {value += s;}, isEmpty: function() { return value === ''; }}; for (i = 0; i < n; i++) { name[i].eval(context).genCSS(context, output); } @@ -19,41 +27,61 @@ function evalName(context, name) { class Declaration extends Node { get type() { return 'Declaration'; } + /** + * @param {string | Node[]} name + * @param {Node | string | null} value + * @param {string} [important] + * @param {string} [merge] + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {boolean} [inline] + * @param {boolean} [variable] + */ constructor(name, value, important, merge, index, currentFileInfo, inline, variable) { super(); this.name = name; this.value = (value instanceof Node) ? value : new Value([value ? new Anonymous(value) : null]); this.important = important ? ` ${important.trim()}` : ''; + /** @type {string | undefined} */ this.merge = merge; this._index = index; this._fileInfo = currentFileInfo; + /** @type {boolean} */ this.inline = inline || false; + /** @type {boolean} */ this.variable = (variable !== undefined) ? variable - : (name.charAt && (name.charAt(0) === '@')); + : (typeof name === 'string' && name.charAt(0) === '@'); + /** @type {boolean} */ this.allowRoot = true; this.setParent(this.value, this); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { - output.add(this.name + (context.compress ? ':' : ': '), this.fileInfo(), this.getIndex()); + output.add(/** @type {string} */ (this.name) + (context.compress ? ':' : ': '), this.fileInfo(), this.getIndex()); try { - this.value.genCSS(context, output); + /** @type {Node} */ (this.value).genCSS(context, output); } catch (e) { - e.index = this._index; - e.filename = this._fileInfo.filename; + const err = /** @type {{ index?: number, filename?: string }} */ (e); + err.index = this._index; + err.filename = this._fileInfo && this._fileInfo.filename; throw e; } output.add(this.important + ((this.inline || (context.lastRule && context.compress)) ? '' : ';'), this._fileInfo, this._index); } + /** @param {EvalContext} context */ eval(context) { let mathBypass = false, prevMath, name = this.name, evaldValue, variable = this.variable; if (typeof name !== 'string') { // expand 'primitive' name directly to get // things faster (~10% for benchmark.less): - name = (name.length === 1) && (name[0] instanceof Keyword) ? - name[0].value : evalName(context, name); + name = (/** @type {Node[]} */ (name).length === 1) && (/** @type {Node[]} */ (name)[0] instanceof Keyword) ? + /** @type {string} */ (/** @type {Node[]} */ (name)[0].value) : evalName(context, /** @type {Node[]} */ (name)); variable = false; // never treat expanded interpolation as new variable name } @@ -65,7 +93,7 @@ class Declaration extends Node { } try { context.importantScope.push({}); - evaldValue = this.value.eval(context); + evaldValue = /** @type {Node} */ (this.value).eval(context); if (!this.variable && evaldValue.type === 'DetachedRuleset') { throw { message: 'Rulesets cannot be evaluated on a property.', @@ -73,11 +101,11 @@ class Declaration extends Node { } let important = this.important; const importantResult = context.importantScope.pop(); - if (!important && importantResult.important) { + if (!important && importantResult && importantResult.important) { important = importantResult.important; } - return new Declaration(name, + return new Declaration(/** @type {string} */ (name), evaldValue, important, this.merge, @@ -85,9 +113,10 @@ class Declaration extends Node { variable); } catch (e) { - if (typeof e.index !== 'number') { - e.index = this.getIndex(); - e.filename = this.fileInfo().filename; + const err = /** @type {{ index?: number, filename?: string }} */ (e); + if (typeof err.index !== 'number') { + err.index = this.getIndex(); + err.filename = this.fileInfo().filename; } throw e; } @@ -100,7 +129,7 @@ class Declaration extends Node { makeImportant() { return new Declaration(this.name, - this.value, + /** @type {Node} */ (this.value), '!important', this.merge, this.getIndex(), this.fileInfo(), this.inline); diff --git a/packages/less/lib/less/tree/detached-ruleset.js b/packages/less/lib/less/tree/detached-ruleset.js index 0f4327941..439bbfa20 100644 --- a/packages/less/lib/less/tree/detached-ruleset.js +++ b/packages/less/lib/less/tree/detached-ruleset.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, TreeVisitor } from './node.js' */ import Node from './node.js'; import contexts from '../contexts.js'; import * as utils from '../utils.js'; @@ -5,6 +7,10 @@ import * as utils from '../utils.js'; class DetachedRuleset extends Node { get type() { return 'DetachedRuleset'; } + /** + * @param {Node} ruleset + * @param {Node[]} [frames] + */ constructor(ruleset, frames) { super(); this.ruleset = ruleset; @@ -13,15 +19,24 @@ class DetachedRuleset extends Node { this.setParent(this.ruleset, this); } + /** @param {TreeVisitor} visitor */ accept(visitor) { this.ruleset = visitor.visit(this.ruleset); } + /** + * @param {EvalContext} context + * @returns {DetachedRuleset} + */ eval(context) { const frames = this.frames || utils.copyArray(context.frames); return new DetachedRuleset(this.ruleset, frames); } + /** + * @param {EvalContext} context + * @returns {Node} + */ callEval(context) { return this.ruleset.eval(this.frames ? new contexts.Eval(context, this.frames.concat(context.frames)) : context); } diff --git a/packages/less/lib/less/tree/dimension.js b/packages/less/lib/less/tree/dimension.js index 7811d6bc5..18815d899 100644 --- a/packages/less/lib/less/tree/dimension.js +++ b/packages/less/lib/less/tree/dimension.js @@ -1,46 +1,64 @@ +// @ts-check /* eslint-disable no-prototype-builtins */ import Node from './node.js'; import unitConversions from '../data/unit-conversions.js'; import Unit from './unit.js'; import Color from './color.js'; +/** @import { EvalContext, CSSOutput } from './node.js' */ + // // A number with a unit // class Dimension extends Node { get type() { return 'Dimension'; } + /** + * @param {number | string} value + * @param {Unit | string} [unit] + */ constructor(value, unit) { super(); - this.value = parseFloat(value); + /** @type {number} */ + this.value = parseFloat(/** @type {string} */ (value)); if (isNaN(this.value)) { throw new Error('Dimension is not a number.'); } + /** @type {Unit} */ this.unit = (unit && unit instanceof Unit) ? unit : - new Unit(unit ? [unit] : undefined); + new Unit(unit ? [/** @type {string} */ (unit)] : undefined); this.setParent(this.unit, this); } + /** + * @param {import('./node.js').TreeVisitor} visitor + */ accept(visitor) { - this.unit = visitor.visit(this.unit); + this.unit = /** @type {Unit} */ (visitor.visit(this.unit)); } // remove when Nodes have JSDoc types // eslint-disable-next-line no-unused-vars + /** @param {EvalContext} context */ eval(context) { return this; } toColor() { - return new Color([this.value, this.value, this.value]); + const v = /** @type {number} */ (this.value); + return new Color([v, v, v]); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { if ((context && context.strictUnits) && !this.unit.isSingular()) { throw new Error(`Multiple units in dimension. Correct the units or use the unit function. Bad unit: ${this.unit.toString()}`); } - const value = this.fround(context, this.value); + const value = this.fround(context, /** @type {number} */ (this.value)); let strValue = String(value); if (value !== 0 && value < 0.000001 && value > -0.000001) { @@ -68,9 +86,14 @@ class Dimension extends Node { // In an operation between two Dimensions, // we default to the first Dimension's unit, // so `1px + 2` will yield `3px`. + /** + * @param {EvalContext} context + * @param {string} op + * @param {Dimension} other + */ operate(context, op, other) { /* jshint noempty:false */ - let value = this._operate(context, op, this.value, other.value); + let value = this._operate(context, op, /** @type {number} */ (this.value), /** @type {number} */ (other.value)); let unit = this.unit.clone(); if (op === '+' || op === '-') { @@ -89,7 +112,7 @@ class Dimension extends Node { + `Bad units: '${unit.toString()}' and '${other.unit.toString()}'.`); } - value = this._operate(context, op, this.value, other.value); + value = this._operate(context, op, /** @type {number} */ (this.value), /** @type {number} */ (other.value)); } } else if (op === '*') { unit.numerator = unit.numerator.concat(other.unit.numerator).sort(); @@ -100,9 +123,13 @@ class Dimension extends Node { unit.denominator = unit.denominator.concat(other.unit.numerator).sort(); unit.cancel(); } - return new Dimension(value, unit); + return new Dimension(/** @type {number} */ (value), unit); } + /** + * @param {Node} other + * @returns {number | undefined} + */ compare(other) { let a, b; @@ -121,26 +148,35 @@ class Dimension extends Node { } } - return Node.numericCompare(a.value, b.value); + return Node.numericCompare(/** @type {number} */ (a.value), /** @type {number} */ (b.value)); } unify() { return this.convertTo({ length: 'px', duration: 's', angle: 'rad' }); } + /** + * @param {string | { [groupName: string]: string }} conversions + * @returns {Dimension} + */ convertTo(conversions) { - let value = this.value; + let value = /** @type {number} */ (this.value); const unit = this.unit.clone(); let i; + /** @type {string} */ let groupName; + /** @type {{ [unitName: string]: number }} */ let group; + /** @type {string} */ let targetUnit; + /** @type {{ [groupName: string]: string }} */ let derivedConversions = {}; + /** @type {(atomicUnit: string, denominator: boolean) => string} */ let applyUnit; if (typeof conversions === 'string') { for (i in unitConversions) { - if (unitConversions[i].hasOwnProperty(conversions)) { + if (unitConversions[/** @type {keyof typeof unitConversions} */ (i)].hasOwnProperty(conversions)) { derivedConversions = {}; derivedConversions[i] = conversions; } @@ -164,7 +200,7 @@ class Dimension extends Node { for (groupName in conversions) { if (conversions.hasOwnProperty(groupName)) { targetUnit = conversions[groupName]; - group = unitConversions[groupName]; + group = /** @type {{ [unitName: string]: number }} */ (unitConversions[/** @type {keyof typeof unitConversions} */ (groupName)]); unit.map(applyUnit); } diff --git a/packages/less/lib/less/tree/element.js b/packages/less/lib/less/tree/element.js index 880d3dc69..d93bf5665 100644 --- a/packages/less/lib/less/tree/element.js +++ b/packages/less/lib/less/tree/element.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */ import Node from './node.js'; import Paren from './paren.js'; import Combinator from './combinator.js'; @@ -5,6 +7,14 @@ import Combinator from './combinator.js'; class Element extends Node { get type() { return 'Element'; } + /** + * @param {Combinator | string} combinator + * @param {string | Node} value + * @param {boolean} [isVariable] + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(combinator, value, isVariable, index, currentFileInfo, visibilityInfo) { super(); this.combinator = combinator instanceof Combinator ? @@ -17,6 +27,7 @@ class Element extends Node { } else { this.value = ''; } + /** @type {boolean | undefined} */ this.isVariable = isVariable; this._index = index; this._fileInfo = currentFileInfo; @@ -24,17 +35,19 @@ class Element extends Node { this.setParent(this.combinator, this); } + /** @param {TreeVisitor} visitor */ accept(visitor) { const value = this.value; - this.combinator = visitor.visit(this.combinator); + this.combinator = /** @type {Combinator} */ (visitor.visit(this.combinator)); if (typeof value === 'object') { - this.value = visitor.visit(value); + this.value = visitor.visit(/** @type {Node} */ (value)); } } + /** @param {EvalContext} context */ eval(context) { return new Element(this.combinator, - this.value.eval ? this.value.eval(context) : this.value, + /** @type {Node} */ (this.value).eval ? /** @type {Node} */ (this.value).eval(context) : /** @type {string} */ (this.value), this.isVariable, this.getIndex(), this.fileInfo(), this.visibilityInfo()); @@ -42,31 +55,37 @@ class Element extends Node { clone() { return new Element(this.combinator, - this.value, + /** @type {string | Node} */ (this.value), this.isVariable, this.getIndex(), this.fileInfo(), this.visibilityInfo()); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add(this.toCSS(context), this.fileInfo(), this.getIndex()); } + /** @param {EvalContext} [context] */ toCSS(context) { - context = context || {}; + /** @type {EvalContext & { firstSelector?: boolean }} */ + const ctx = context || {}; let value = this.value; - const firstSelector = context.firstSelector; + const firstSelector = ctx.firstSelector; if (value instanceof Paren) { // selector in parens should not be affected by outer selector // flags (breaks only interpolated selectors - see #1973) - context.firstSelector = true; + ctx.firstSelector = true; } - value = value.toCSS ? value.toCSS(context) : value; - context.firstSelector = firstSelector; + value = /** @type {Node} */ (value).toCSS ? /** @type {Node} */ (value).toCSS(ctx) : /** @type {string} */ (value); + ctx.firstSelector = firstSelector; if (value === '' && this.combinator.value.charAt(0) === '&') { return ''; } else { - return this.combinator.toCSS(context) + value; + return this.combinator.toCSS(ctx) + value; } } } diff --git a/packages/less/lib/less/tree/expression.js b/packages/less/lib/less/tree/expression.js index e2ff11c57..1f748ec15 100644 --- a/packages/less/lib/less/tree/expression.js +++ b/packages/less/lib/less/tree/expression.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */ import Node from './node.js'; import Paren from './paren.js'; import Comment from './comment.js'; @@ -7,21 +9,33 @@ import Anonymous from './anonymous.js'; class Expression extends Node { get type() { return 'Expression'; } + /** + * @param {Node[]} value + * @param {boolean} [noSpacing] + */ constructor(value, noSpacing) { super(); this.value = value; + /** @type {boolean | undefined} */ this.noSpacing = noSpacing; + /** @type {boolean | undefined} */ + this.parens = undefined; + /** @type {boolean | undefined} */ + this.parensInOp = undefined; if (!value) { throw new Error('Expression requires an array parameter'); } } + /** @param {TreeVisitor} visitor */ accept(visitor) { - this.value = visitor.visitArray(this.value); + this.value = visitor.visitArray(/** @type {Node[]} */ (this.value)); } + /** @param {EvalContext} context */ eval(context) { const noSpacing = this.noSpacing; + /** @type {Node | Expression} */ let returnValue; const mathOn = context.isMathOn(); const inParenthesis = this.parens; @@ -30,18 +44,20 @@ class Expression extends Node { if (inParenthesis) { context.inParenthesis(); } - if (this.value.length > 1) { - returnValue = new Expression(this.value.map(function (e) { + const value = /** @type {Node[]} */ (this.value); + if (value.length > 1) { + returnValue = new Expression(value.map(function (e) { if (!e.eval) { return e; } return e.eval(context); }), this.noSpacing); - } else if (this.value.length === 1) { - if (this.value[0].parens && !this.value[0].parensInOp && !context.inCalc) { + } else if (value.length === 1) { + const first = /** @type {Expression} */ (value[0]); + if (first.parens && !first.parensInOp && !context.inCalc) { doubleParen = true; } - returnValue = this.value[0].eval(context); + returnValue = value[0].eval(context); } else { returnValue = this; } @@ -52,16 +68,22 @@ class Expression extends Node { && (!(returnValue instanceof Dimension))) { returnValue = new Paren(returnValue); } - returnValue.noSpacing = returnValue.noSpacing || noSpacing; + /** @type {Expression} */ (returnValue).noSpacing = + /** @type {Expression} */ (returnValue).noSpacing || noSpacing; return returnValue; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { - for (let i = 0; i < this.value.length; i++) { - this.value[i].genCSS(context, output); - if (!this.noSpacing && i + 1 < this.value.length) { - if (!(this.value[i + 1] instanceof Anonymous) || - this.value[i + 1] instanceof Anonymous && this.value[i + 1].value !== ',') { + const value = /** @type {Node[]} */ (this.value); + for (let i = 0; i < value.length; i++) { + value[i].genCSS(context, output); + if (!this.noSpacing && i + 1 < value.length) { + if (!(value[i + 1] instanceof Anonymous) || + value[i + 1] instanceof Anonymous && /** @type {string} */ (value[i + 1].value) !== ',') { output.add(' '); } } @@ -69,7 +91,7 @@ class Expression extends Node { } throwAwayComments() { - this.value = this.value.filter(function(v) { + this.value = /** @type {Node[]} */ (this.value).filter(function(v) { return !(v instanceof Comment); }); } diff --git a/packages/less/lib/less/tree/extend.js b/packages/less/lib/less/tree/extend.js index 59f8de370..59aa4e680 100644 --- a/packages/less/lib/less/tree/extend.js +++ b/packages/less/lib/less/tree/extend.js @@ -1,19 +1,34 @@ +// @ts-check +/** @import { EvalContext, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */ import Node from './node.js'; import Selector from './selector.js'; class Extend extends Node { get type() { return 'Extend'; } + /** + * @param {Selector} selector + * @param {string} option + * @param {number} index + * @param {FileInfo} currentFileInfo + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(selector, option, index, currentFileInfo, visibilityInfo) { super(); this.selector = selector; this.option = option; this.object_id = Extend.next_id++; + /** @type {number[]} */ this.parent_ids = [this.object_id]; this._index = index; this._fileInfo = currentFileInfo; this.copyVisibilityInfo(visibilityInfo); + /** @type {boolean} */ this.allowRoot = true; + /** @type {boolean} */ + this.allowBefore = false; + /** @type {boolean} */ + this.allowAfter = false; switch (option) { case '!all': @@ -29,23 +44,29 @@ class Extend extends Node { this.setParent(this.selector, this); } + /** @param {TreeVisitor} visitor */ accept(visitor) { - this.selector = visitor.visit(this.selector); + this.selector = /** @type {Selector} */ (visitor.visit(this.selector)); } + /** @param {EvalContext} context */ eval(context) { - return new Extend(this.selector.eval(context), this.option, this.getIndex(), this.fileInfo(), this.visibilityInfo()); + return new Extend(/** @type {Selector} */ (this.selector.eval(context)), this.option, this.getIndex(), this.fileInfo(), this.visibilityInfo()); } // remove when Nodes have JSDoc types // eslint-disable-next-line no-unused-vars + /** @param {EvalContext} [context] */ clone(context) { return new Extend(this.selector, this.option, this.getIndex(), this.fileInfo(), this.visibilityInfo()); } // it concatenates (joins) all selectors in selector array + /** @param {Selector[]} selectors */ findSelfSelectors(selectors) { - let selfElements = [], i, selectorElements; + /** @type {import('./element.js').default[]} */ + let selfElements = []; + let i, selectorElements; for (i = 0; i < selectors.length; i++) { selectorElements = selectors[i].elements; @@ -57,6 +78,7 @@ class Extend extends Node { selfElements = selfElements.concat(selectors[i].elements); } + /** @type {Selector[]} */ this.selfSelectors = [new Selector(selfElements)]; this.selfSelectors[0].copyVisibilityInfo(this.visibilityInfo()); } diff --git a/packages/less/lib/less/tree/import.js b/packages/less/lib/less/tree/import.js index d7107aeb5..ad734560c 100644 --- a/packages/less/lib/less/tree/import.js +++ b/packages/less/lib/less/tree/import.js @@ -1,12 +1,22 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */ import Node from './node.js'; import Media from './media.js'; import URL from './url.js'; import Quoted from './quoted.js'; import Ruleset from './ruleset.js'; import Anonymous from './anonymous.js'; +import Expression from './expression.js'; import * as utils from '../utils.js'; import LessError from '../less-error.js'; -import Expression from './expression.js'; + +/** + * @typedef {object} ImportOptions + * @property {boolean} [less] + * @property {boolean} [inline] + * @property {boolean} [isPlugin] + * @property {boolean} [reference] + */ // // CSS @import node @@ -23,15 +33,39 @@ import Expression from './expression.js'; class Import extends Node { get type() { return 'Import'; } + /** + * @param {Node} path + * @param {Node | null} features + * @param {ImportOptions} options + * @param {number} index + * @param {FileInfo} [currentFileInfo] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(path, features, options, index, currentFileInfo, visibilityInfo) { super(); + /** @type {ImportOptions} */ this.options = options; this._index = index; this._fileInfo = currentFileInfo; this.path = path; + /** @type {Node | null} */ this.features = features; + /** @type {boolean} */ this.allowRoot = true; + /** @type {boolean | undefined} */ + this.css = undefined; + /** @type {boolean | undefined} */ + this.layerCss = undefined; + /** @type {(Ruleset & { imports?: object, filename?: string, functions?: object, functionRegistry?: { addMultiple: (fns: object) => void } }) | undefined} */ + this.root = undefined; + /** @type {string | undefined} */ + this.importedFilename = undefined; + /** @type {boolean | (() => boolean) | undefined} */ + this.skip = undefined; + /** @type {{ message: string, index: number, filename: string } | undefined} */ + this.error = undefined; + if (this.options.less !== undefined || this.options.inline) { this.css = !this.options.less || this.options.inline; } else { @@ -41,20 +75,27 @@ class Import extends Node { } } this.copyVisibilityInfo(visibilityInfo); - this.setParent(this.features, this); - this.setParent(this.path, this); + if (this.features) { + this.setParent(this.features, /** @type {Node} */ (this)); + } + this.setParent(this.path, /** @type {Node} */ (this)); } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.features) { this.features = visitor.visit(this.features); } this.path = visitor.visit(this.path); if (!this.options.isPlugin && !this.options.inline && this.root) { - this.root = visitor.visit(this.root); + this.root = /** @type {Ruleset} */ (visitor.visit(this.root)); } } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { if (this.css && this.path._fileInfo.reference === undefined) { output.add('@import ', this._fileInfo, this._index); @@ -67,15 +108,18 @@ class Import extends Node { } } + /** @returns {string | undefined} */ getPath() { return (this.path instanceof URL) ? - this.path.value.value : this.path.value; + /** @type {string} */ (/** @type {Node} */ (this.path.value).value) : + /** @type {string | undefined} */ (this.path.value); } + /** @returns {boolean | RegExpMatchArray | null} */ isVariableImport() { let path = this.path; if (path instanceof URL) { - path = path.value; + path = /** @type {Node} */ (path.value); } if (path instanceof Quoted) { return path.containsVariables(); @@ -84,53 +128,57 @@ class Import extends Node { return true; } + /** @param {EvalContext} context */ evalForImport(context) { let path = this.path; if (path instanceof URL) { - path = path.value; + path = /** @type {Node} */ (path.value); } - return new Import(path.eval(context), this.features, this.options, this._index, this._fileInfo, this.visibilityInfo()); + return new Import(path.eval(context), this.features, this.options, this._index || 0, this._fileInfo, this.visibilityInfo()); } + /** @param {EvalContext} context */ evalPath(context) { const path = this.path.eval(context); const fileInfo = this._fileInfo; if (!(path instanceof URL)) { // Add the rootpath if the URL requires a rewrite - const pathValue = path.value; + const pathValue = /** @type {string} */ (path.value); if (fileInfo && pathValue && context.pathRequiresRewrite(pathValue)) { path.value = context.rewritePath(pathValue, fileInfo.rootpath); } else { - path.value = context.normalizePath(path.value); + path.value = context.normalizePath(/** @type {string} */ (path.value)); } } return path; } + /** @param {EvalContext} context */ + // @ts-ignore - Import.eval returns Node | Node[] | Import (wider than Node.eval's Node return) eval(context) { const result = this.doEval(context); if (this.options.reference || this.blocksVisibility()) { - if (result.length || result.length === 0) { + if (Array.isArray(result)) { result.forEach(function (node) { node.addVisibilityBlock(); - } - ); + }); } else { - result.addVisibilityBlock(); + /** @type {Node} */ (result).addVisibilityBlock(); } } return result; } + /** @param {EvalContext} context */ doEval(context) { + /** @type {Ruleset | undefined} */ let ruleset; - let registry; const features = this.features && this.features.eval(context); if (this.options.isPlugin) { @@ -139,13 +187,19 @@ class Import extends Node { this.root.eval(context); } catch (e) { - e.message = 'Plugin error during evaluation'; - throw new LessError(e, this.root.imports, this.root.filename); + const err = /** @type {{ message: string }} */ (e); + err.message = 'Plugin error during evaluation'; + throw new LessError( + /** @type {{ message: string, index?: number, filename?: string }} */ (e), + /** @type {{ imports: object }} */ (/** @type {unknown} */ (this.root)).imports, + /** @type {{ filename: string }} */ (/** @type {unknown} */ (this.root)).filename + ); } } - registry = context.frames[0] && context.frames[0].functionRegistry; - if ( registry && this.root && this.root.functions ) { - registry.addMultiple( this.root.functions ); + const frame0 = /** @type {Ruleset & { functionRegistry?: { addMultiple: (fns: object) => void } }} */ (context.frames[0]); + const registry = frame0 && frame0.functionRegistry; + if (registry && this.root && this.root.functions) { + registry.addMultiple(this.root.functions); } return []; @@ -160,11 +214,11 @@ class Import extends Node { } } if (this.features) { - let featureValue = this.features.value; + let featureValue = /** @type {Node[]} */ (this.features.value); if (Array.isArray(featureValue) && featureValue.length >= 1) { const expr = featureValue[0]; - if (expr.type === 'Expression' && Array.isArray(expr.value) && expr.value.length >= 2) { - featureValue = expr.value; + if (expr.type === 'Expression' && Array.isArray(expr.value) && /** @type {Node[]} */ (expr.value).length >= 2) { + featureValue = /** @type {Node[]} */ (expr.value); const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer' && featureValue[1].type === 'Paren'; if (isLayer) { @@ -174,15 +228,20 @@ class Import extends Node { } } if (this.options.inline) { - const contents = new Anonymous(this.root, 0, + const contents = new Anonymous( + /** @type {string} */ (/** @type {unknown} */ (this.root)), + 0, { filename: this.importedFilename, reference: this.path._fileInfo && this.path._fileInfo.reference - }, true, true); + }, + true, + true + ); - return this.features ? new Media([contents], this.features.value) : [contents]; + return this.features ? new Media([contents], /** @type {Node[]} */ (this.features.value)) : [contents]; } else if (this.css || this.layerCss) { - const newImport = new Import(this.evalPath(context), features, this.options, this._index); + const newImport = new Import(this.evalPath(context), features, this.options, this._index || 0); if (this.layerCss) { newImport.css = this.layerCss; newImport.path._fileInfo = this._fileInfo; @@ -193,18 +252,18 @@ class Import extends Node { return newImport; } else if (this.root) { if (this.features) { - let featureValue = this.features.value; + let featureValue = /** @type {Node[]} */ (this.features.value); if (Array.isArray(featureValue) && featureValue.length === 1) { const expr = featureValue[0]; - if (expr.type === 'Expression' && Array.isArray(expr.value) && expr.value.length >= 2) { - featureValue = expr.value; + if (expr.type === 'Expression' && Array.isArray(expr.value) && /** @type {Node[]} */ (expr.value).length >= 2) { + featureValue = /** @type {Node[]} */ (expr.value); const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer' && featureValue[1].type === 'Paren'; if (isLayer) { this.layerCss = true; - featureValue[0] = new Expression(featureValue.slice(0, 2)); + featureValue[0] = new Expression(/** @type {Node[]} */ (featureValue.slice(0, 2))); featureValue.splice(1, 1); - featureValue[0].noSpacing = true; + /** @type {Expression} */ (featureValue[0]).noSpacing = true; return this; } } @@ -213,20 +272,20 @@ class Import extends Node { ruleset = new Ruleset(null, utils.copyArray(this.root.rules)); ruleset.evalImports(context); - return this.features ? new Media(ruleset.rules, this.features.value) : ruleset.rules; + return this.features ? new Media(ruleset.rules, /** @type {Node[]} */ (this.features.value)) : ruleset.rules; } else { if (this.features) { - let featureValue = this.features.value; + let featureValue = /** @type {Node[]} */ (this.features.value); if (Array.isArray(featureValue) && featureValue.length >= 1) { - featureValue = featureValue[0].value; + featureValue = /** @type {Node[]} */ (featureValue[0].value); if (Array.isArray(featureValue) && featureValue.length >= 2) { const isLayer = featureValue[0].type === 'Keyword' && featureValue[0].value === 'layer' && featureValue[1].type === 'Paren'; if (isLayer) { this.css = true; - featureValue[0] = new Expression(featureValue.slice(0, 2)); + featureValue[0] = new Expression(/** @type {Node[]} */ (featureValue.slice(0, 2))); featureValue.splice(1, 1); - featureValue[0].noSpacing = true; + /** @type {Expression} */ (featureValue[0]).noSpacing = true; return this; } } diff --git a/packages/less/lib/less/tree/index.js b/packages/less/lib/less/tree/index.js index a6460b709..87704f84e 100644 --- a/packages/less/lib/less/tree/index.js +++ b/packages/less/lib/less/tree/index.js @@ -1,3 +1,4 @@ +// @ts-check import Node from './node.js'; import Color from './color.js'; import AtRule from './atrule.js'; @@ -45,11 +46,11 @@ export default { Ruleset, Element, Attribute, Combinator, Selector, Quoted, Expression, Declaration, Call, URL, Import, Comment, Anonymous, Value, JavaScript, Assignment, - Condition, Paren, Media, Container, QueryInParens, - UnicodeDescriptor, Negative, Extend, VariableCall, + Condition, Paren, Media, Container, QueryInParens, + UnicodeDescriptor, Negative, Extend, VariableCall, NamespaceValue, mixin: { Call: MixinCall, Definition: MixinDefinition } -}; \ No newline at end of file +}; diff --git a/packages/less/lib/less/tree/javascript.js b/packages/less/lib/less/tree/javascript.js index d8cadc756..98cf75761 100644 --- a/packages/less/lib/less/tree/javascript.js +++ b/packages/less/lib/less/tree/javascript.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, FileInfo } from './node.js' */ import JsEvalNode from './js-eval-node.js'; import Dimension from './dimension.js'; import Quoted from './quoted.js'; @@ -6,6 +8,12 @@ import Anonymous from './anonymous.js'; class JavaScript extends JsEvalNode { get type() { return 'JavaScript'; } + /** + * @param {string} string + * @param {boolean} escaped + * @param {number} index + * @param {FileInfo} currentFileInfo + */ constructor(string, escaped, index, currentFileInfo) { super(); this.escaped = escaped; @@ -14,18 +22,22 @@ class JavaScript extends JsEvalNode { this._fileInfo = currentFileInfo; } + /** + * @param {EvalContext} context + * @returns {Dimension | Quoted | Anonymous} + */ eval(context) { const result = this.evaluateJavaScript(this.expression, context); const type = typeof result; - if (type === 'number' && !isNaN(result)) { - return new Dimension(result); + if (type === 'number' && !isNaN(/** @type {number} */ (result))) { + return new Dimension(/** @type {number} */ (result)); } else if (type === 'string') { - return new Quoted(`"${result}"`, result, this.escaped, this._index); + return new Quoted(`"${result}"`, /** @type {string} */ (result), this.escaped, this._index); } else if (Array.isArray(result)) { - return new Anonymous(result.join(', ')); + return new Anonymous(/** @type {string[]} */ (result).join(', ')); } else { - return new Anonymous(result); + return new Anonymous(/** @type {string} */ (result)); } } } diff --git a/packages/less/lib/less/tree/js-eval-node.js b/packages/less/lib/less/tree/js-eval-node.js index 2cf85fb93..5732ecb9a 100644 --- a/packages/less/lib/less/tree/js-eval-node.js +++ b/packages/less/lib/less/tree/js-eval-node.js @@ -1,10 +1,18 @@ +// @ts-check +/** @import { EvalContext } from './node.js' */ import Node from './node.js'; import Variable from './variable.js'; class JsEvalNode extends Node { + /** + * @param {string} expression + * @param {EvalContext} context + * @returns {string | number | boolean} + */ evaluateJavaScript(expression, context) { let result; const that = this; + /** @type {Record string }>} */ const evalContext = {}; if (!context.javascriptEnabled) { @@ -17,42 +25,48 @@ class JsEvalNode extends Node { return that.jsify(new Variable(`@${name}`, that.getIndex(), that.fileInfo()).eval(context)); }); + /** @type {Function} */ + let expressionFunc; try { - expression = new Function(`return (${expression})`); + expressionFunc = new Function(`return (${expression})`); } catch (e) { - throw { message: `JavaScript evaluation error: ${e.message} from \`${expression}\`` , + throw { message: `JavaScript evaluation error: ${/** @type {Error} */ (e).message} from \`${expression}\`` , filename: this.fileInfo().filename, index: this.getIndex() }; } - const variables = context.frames[0].variables(); + const variables = /** @type {Node & { variables: () => Record }} */ (context.frames[0]).variables(); for (const k in variables) { // eslint-disable-next-line no-prototype-builtins if (variables.hasOwnProperty(k)) { evalContext[k.slice(1)] = { value: variables[k].value, toJS: function () { - return this.value.eval(context).toCSS(); + return this.value.eval(context).toCSS(context); } }; } } try { - result = expression.call(evalContext); + result = expressionFunc.call(evalContext); } catch (e) { - throw { message: `JavaScript evaluation error: '${e.name}: ${e.message.replace(/["]/g, '\'')}'` , + throw { message: `JavaScript evaluation error: '${/** @type {Error} */ (e).name}: ${/** @type {Error} */ (e).message.replace(/["]/g, '\'')}'` , filename: this.fileInfo().filename, index: this.getIndex() }; } return result; } + /** + * @param {Node} obj + * @returns {string} + */ jsify(obj) { if (Array.isArray(obj.value) && (obj.value.length > 1)) { - return `[${obj.value.map(function (v) { return v.toCSS(); }).join(', ')}]`; + return `[${obj.value.map(function (v) { return v.toCSS(/** @type {EvalContext} */ (undefined)); }).join(', ')}]`; } else { - return obj.toCSS(); + return obj.toCSS(/** @type {EvalContext} */ (undefined)); } } } diff --git a/packages/less/lib/less/tree/keyword.js b/packages/less/lib/less/tree/keyword.js index 51041852a..03a925c5b 100644 --- a/packages/less/lib/less/tree/keyword.js +++ b/packages/less/lib/less/tree/keyword.js @@ -1,16 +1,24 @@ +// @ts-check import Node from './node.js'; +/** @import { EvalContext, CSSOutput } from './node.js' */ + class Keyword extends Node { get type() { return 'Keyword'; } + /** @param {string} value */ constructor(value) { super(); this.value = value; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { if (this.value === '%') { throw { type: 'Syntax', message: 'Invalid % without number' }; } - output.add(this.value); + output.add(/** @type {string} */ (this.value)); } } diff --git a/packages/less/lib/less/tree/media.js b/packages/less/lib/less/tree/media.js index 11fd49a69..271924144 100644 --- a/packages/less/lib/less/tree/media.js +++ b/packages/less/lib/less/tree/media.js @@ -1,3 +1,8 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */ +/** @import { NestableAtRuleThis } from './nested-at-rule.js' */ +/** @import { RulesetLikeNode } from './atrule.js' */ +import Node from './node.js'; import Ruleset from './ruleset.js'; import Value from './value.js'; import Selector from './selector.js'; @@ -7,6 +12,13 @@ import NestableAtRulePrototype from './nested-at-rule.js'; class Media extends AtRule { get type() { return 'Media'; } + /** + * @param {Node[] | null} value + * @param {Node[]} features + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(value, features, index, currentFileInfo, visibilityInfo) { super(); this._index = index; @@ -14,22 +26,32 @@ class Media extends AtRule { const selectors = (new Selector([], null, null, this._index, this._fileInfo)).createEmptySelectors(); + /** @type {Value} */ this.features = new Value(features); + /** @type {RulesetLikeNode[]} */ this.rules = [new Ruleset(selectors, value)]; - this.rules[0].allowImports = true; + /** @type {RulesetLikeNode} */ (this.rules[0]).allowImports = true; this.copyVisibilityInfo(visibilityInfo); this.allowRoot = true; - this.setParent(selectors, this); - this.setParent(this.features, this); - this.setParent(this.rules, this); + this.setParent(selectors, /** @type {Node} */ (/** @type {unknown} */ (this))); + this.setParent(this.features, /** @type {Node} */ (/** @type {unknown} */ (this))); + this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this))); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add('@media ', this._fileInfo, this._index); this.features.genCSS(context, output); this.outputRuleset(context, output, this.rules); } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { if (!context.mediaBlocks) { context.mediaBlocks = []; @@ -38,24 +60,24 @@ class Media extends AtRule { const media = new Media(null, [], this._index, this._fileInfo, this.visibilityInfo()); if (this.debugInfo) { - this.rules[0].debugInfo = this.debugInfo; + /** @type {RulesetLikeNode} */ (this.rules[0]).debugInfo = this.debugInfo; media.debugInfo = this.debugInfo; } - media.features = this.features.eval(context); + media.features = /** @type {Value} */ (this.features.eval(context)); - context.mediaPath.push(media); - context.mediaBlocks.push(media); + context.mediaPath.push(/** @type {Node} */ (/** @type {unknown} */ (media))); + context.mediaBlocks.push(/** @type {Node} */ (/** @type {unknown} */ (media))); - this.rules[0].functionRegistry = context.frames[0].functionRegistry.inherit(); + /** @type {RulesetLikeNode} */ (this.rules[0]).functionRegistry = /** @type {RulesetLikeNode} */ (context.frames[0]).functionRegistry.inherit(); context.frames.unshift(this.rules[0]); - media.rules = [this.rules[0].eval(context)]; + media.rules = [/** @type {RulesetLikeNode} */ (this.rules[0].eval(context))]; context.frames.shift(); context.mediaPath.pop(); - return context.mediaPath.length === 0 ? media.evalTop(context) : - media.evalNested(context); + return context.mediaPath.length === 0 ? /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalTop(context) : + /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (media)).evalNested(context); } } diff --git a/packages/less/lib/less/tree/merge-rules.js b/packages/less/lib/less/tree/merge-rules.js index 10f48e105..befdfcaf3 100644 --- a/packages/less/lib/less/tree/merge-rules.js +++ b/packages/less/lib/less/tree/merge-rules.js @@ -1,31 +1,37 @@ +// @ts-check import Expression from './expression.js'; import Value from './value.js'; +import Node from './node.js'; /** * Merges declarations with merge flags (+ or ,) into combined values. * Used by both the ToCSSVisitor and AtRule eval. + * @param {Node[]} rules */ export default function mergeRules(rules) { if (!rules) { return; } + /** @type {Record>} */ const groups = {}; + /** @type {Array>} */ const groupsArr = []; for (let i = 0; i < rules.length; i++) { - const rule = rules[i]; + const rule = /** @type {Node & { merge: string, name: string }} */ (rules[i]); if (rule.merge) { const key = rule.name; groups[key] ? rules.splice(i--, 1) : groupsArr.push(groups[key] = []); - groups[key].push(rule); + groups[key].push(/** @type {Node & { merge: string, name: string, value: Node, important: string }} */ (rule)); } } groupsArr.forEach(group => { if (group.length > 0) { const result = group[0]; + /** @type {Node[]} */ let space = []; const comma = [new Expression(space)]; group.forEach(rule => { diff --git a/packages/less/lib/less/tree/mixin-call.js b/packages/less/lib/less/tree/mixin-call.js index 2e0cb9bd8..1e8aa9bd9 100644 --- a/packages/less/lib/less/tree/mixin-call.js +++ b/packages/less/lib/less/tree/mixin-call.js @@ -1,61 +1,123 @@ +// @ts-check +/** @import { EvalContext, TreeVisitor, FileInfo } from './node.js' */ +/** @import { FunctionRegistry } from './nested-at-rule.js' */ import Node from './node.js'; import Selector from './selector.js'; import MixinDefinition from './mixin-definition.js'; import defaultFunc from '../functions/default.js'; +/** + * @typedef {{ name?: string, value: Node, expand?: boolean }} MixinArg + */ + +/** + * @typedef {Node & { + * rules?: Node[], + * selectors?: Selector[], + * originalRuleset?: Node, + * matchArgs: (args: MixinArg[] | null, context: EvalContext) => boolean, + * matchCondition?: (args: MixinArg[] | null, context: EvalContext) => boolean, + * find: (selector: Selector, self?: Node | null, filter?: (rule: Node) => boolean) => Array<{ rule: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, path: Node[] }>, + * functionRegistry?: FunctionRegistry + * }} MixinSearchFrame + */ + class MixinCall extends Node { get type() { return 'MixinCall'; } + /** + * @param {import('./element.js').default[]} elements + * @param {MixinArg[]} [args] + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {string} [important] + */ constructor(elements, args, index, currentFileInfo, important) { super(); + /** @type {Selector} */ this.selector = new Selector(elements); + /** @type {MixinArg[]} */ this.arguments = args || []; this._index = index; this._fileInfo = currentFileInfo; + /** @type {string | undefined} */ this.important = important; this.allowRoot = true; - this.setParent(this.selector, this); + this.setParent(this.selector, /** @type {Node} */ (/** @type {unknown} */ (this))); } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.selector) { - this.selector = visitor.visit(this.selector); + this.selector = /** @type {Selector} */ (visitor.visit(this.selector)); } if (this.arguments.length) { - this.arguments = visitor.visitArray(this.arguments); + this.arguments = /** @type {MixinArg[]} */ (/** @type {unknown} */ (visitor.visitArray(/** @type {Node[]} */ (/** @type {unknown} */ (this.arguments))))); } } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { + /** @type {{ rule: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, path: Node[] }[] | undefined} */ let mixins; + /** @type {Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }} */ let mixin; + /** @type {Node[]} */ let mixinPath; + /** @type {MixinArg[]} */ const args = []; + /** @type {MixinArg} */ let arg; + /** @type {Node} */ let argValue; + /** @type {Node[]} */ const rules = []; let match = false; + /** @type {number} */ let i; + /** @type {number} */ let m; + /** @type {number} */ let f; + /** @type {boolean} */ let isRecursive; + /** @type {boolean | undefined} */ let isOneFound; + /** @type {{ mixin: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, group: number }[]} */ const candidates = []; + /** @type {{ mixin: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, group: number } | number} */ let candidate; + /** @type {boolean[]} */ const conditionResult = []; + /** @type {number | undefined} */ let defaultResult; const defFalseEitherCase = -1; const defNone = 0; const defTrue = 1; const defFalse = 2; + /** @type {number[]} */ let count; + /** @type {Node | undefined} */ let originalRuleset; + /** @type {((rule: MixinSearchFrame) => boolean) | undefined} */ let noArgumentsFilter; - this.selector = this.selector.eval(context); + this.selector = /** @type {Selector} */ (this.selector.eval(context)); + /** + * @param {Node & { matchCondition?: Function }} mixin + * @param {Node[]} mixinPath + */ function calcDefGroup(mixin, mixinPath) { - let f, p, namespace; + /** @type {number} */ + let f; + /** @type {number} */ + let p; + /** @type {Node & { matchCondition?: Function }} */ + let namespace; for (f = 0; f < 2; f++) { conditionResult[f] = true; @@ -85,19 +147,19 @@ class MixinCall extends Node { arg = this.arguments[i]; argValue = arg.value.eval(context); if (arg.expand && Array.isArray(argValue.value)) { - argValue = argValue.value; - for (m = 0; m < argValue.length; m++) { - args.push({value: argValue[m]}); + const expandedValues = /** @type {Node[]} */ (argValue.value); + for (m = 0; m < expandedValues.length; m++) { + args.push({value: expandedValues[m]}); } } else { args.push({name: arg.name, value: argValue}); } } - noArgumentsFilter = function(rule) {return rule.matchArgs(null, context);}; + noArgumentsFilter = function(/** @type {MixinSearchFrame} */ rule) {return rule.matchArgs(null, context);}; for (i = 0; i < context.frames.length; i++) { - if ((mixins = context.frames[i].find(this.selector, null, noArgumentsFilter)).length > 0) { + if ((mixins = /** @type {MixinSearchFrame} */ (context.frames[i]).find(this.selector, null, /** @type {(rule: Node) => boolean} */ (/** @type {unknown} */ (noArgumentsFilter)))).length > 0) { isOneFound = true; // To make `default()` function independent of definition order we have two "subpasses" here. @@ -110,7 +172,7 @@ class MixinCall extends Node { mixinPath = mixins[m].path; isRecursive = false; for (f = 0; f < context.frames.length; f++) { - if ((!(mixin instanceof MixinDefinition)) && mixin === (context.frames[f].originalRuleset || context.frames[f])) { + if ((!(mixin instanceof MixinDefinition)) && mixin === (/** @type {Node & { originalRuleset?: Node }} */ (context.frames[f]).originalRuleset || context.frames[f])) { isRecursive = true; break; } @@ -122,8 +184,8 @@ class MixinCall extends Node { if (mixin.matchArgs(args, context)) { candidate = {mixin, group: calcDefGroup(mixin, mixinPath)}; - if (candidate.group !== defFalseEitherCase) { - candidates.push(candidate); + if (/** @type {{ mixin: Node, group: number }} */ (candidate).group !== defFalseEitherCase) { + candidates.push(/** @type {{ mixin: Node & { rules?: Node[], originalRuleset?: Node, matchArgs: Function, matchCondition?: Function, evalCall?: Function }, group: number }} */ (candidate)); } match = true; @@ -154,21 +216,22 @@ class MixinCall extends Node { try { mixin = candidates[m].mixin; if (!(mixin instanceof MixinDefinition)) { - originalRuleset = mixin.originalRuleset || mixin; + originalRuleset = /** @type {Node & { originalRuleset?: Node }} */ (mixin).originalRuleset || mixin; mixin = new MixinDefinition('', [], mixin.rules, null, false, null, originalRuleset.visibilityInfo()); - mixin.originalRuleset = originalRuleset; + /** @type {Node & { originalRuleset?: Node }} */ (mixin).originalRuleset = originalRuleset; } - const newRules = mixin.evalCall(context, args, this.important).rules; + const newRules = /** @type {MixinDefinition} */ (mixin).evalCall(context, args, this.important).rules; this._setVisibilityToReplacement(newRules); Array.prototype.push.apply(rules, newRules); } catch (e) { - throw { message: e.message, index: this.getIndex(), filename: this.fileInfo().filename, stack: e.stack }; + const err = /** @type {{ message?: string, stack?: string }} */ (e); + throw { message: err.message, index: this.getIndex(), filename: this.fileInfo().filename, stack: err.stack }; } } } if (match) { - return rules; + return /** @type {Node} */ (/** @type {unknown} */ (rules)); } } } @@ -178,13 +241,17 @@ class MixinCall extends Node { index: this.getIndex(), filename: this.fileInfo().filename }; } else { throw { type: 'Name', - message: `${this.selector.toCSS().trim()} is undefined`, + message: `${this.selector.toCSS(/** @type {EvalContext} */ ({})).trim()} is undefined`, index: this.getIndex(), filename: this.fileInfo().filename }; } } + /** @param {Node[]} replacement */ _setVisibilityToReplacement(replacement) { - let i, rule; + /** @type {number} */ + let i; + /** @type {Node} */ + let rule; if (this.blocksVisibility()) { for (i = 0; i < replacement.length; i++) { rule = replacement[i]; @@ -193,14 +260,15 @@ class MixinCall extends Node { } } + /** @param {MixinArg[]} args */ format(args) { - return `${this.selector.toCSS().trim()}(${args ? args.map(function (a) { + return `${this.selector.toCSS(/** @type {EvalContext} */ ({})).trim()}(${args ? args.map(function (/** @type {MixinArg} */ a) { let argValue = ''; if (a.name) { argValue += `${a.name}:`; } if (a.value.toCSS) { - argValue += a.value.toCSS(); + argValue += a.value.toCSS(/** @type {EvalContext} */ ({})); } else { argValue += '???'; } diff --git a/packages/less/lib/less/tree/mixin-definition.js b/packages/less/lib/less/tree/mixin-definition.js index f99659980..b63f05aa6 100644 --- a/packages/less/lib/less/tree/mixin-definition.js +++ b/packages/less/lib/less/tree/mixin-definition.js @@ -1,3 +1,8 @@ +// @ts-check +/** @import { EvalContext, TreeVisitor, VisibilityInfo } from './node.js' */ +/** @import { FunctionRegistry } from './nested-at-rule.js' */ +/** @import { MixinArg } from './mixin-call.js' */ +import Node from './node.js'; import Selector from './selector.js'; import Element from './element.js'; import Ruleset from './ruleset.js'; @@ -7,21 +12,51 @@ import Expression from './expression.js'; import contexts from '../contexts.js'; import * as utils from '../utils.js'; +/** + * @typedef {object} MixinParam + * @property {string} [name] + * @property {Node} [value] + * @property {boolean} [variadic] + */ + +/** + * @typedef {Ruleset & { + * functionRegistry?: FunctionRegistry, + * originalRuleset?: Node + * }} RulesetWithRegistry + */ + class Definition extends Ruleset { get type() { return 'MixinDefinition'; } + /** + * @param {string | undefined} name + * @param {MixinParam[]} params + * @param {Node[]} rules + * @param {Node | null} [condition] + * @param {boolean} [variadic] + * @param {Node[] | null} [frames] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(name, params, rules, condition, variadic, frames, visibilityInfo) { - super(); + super(null, null); + /** @type {string} */ this.name = name || 'anonymous mixin'; this.selectors = [new Selector([new Element(null, name, false, this._index, this._fileInfo)])]; + /** @type {MixinParam[]} */ this.params = params; + /** @type {Node | null | undefined} */ this.condition = condition; + /** @type {boolean | undefined} */ this.variadic = variadic; + /** @type {number} */ this.arity = params.length; this.rules = rules; this._lookups = {}; + /** @type {string[]} */ const optionalParameters = []; - this.required = params.reduce(function (count, p) { + /** @type {number} */ + this.required = params.reduce(function (/** @type {number} */ count, /** @type {MixinParam} */ p) { if (!p.name || (p.name && !p.value)) { return count + 1; } @@ -30,16 +65,20 @@ class Definition extends Ruleset { return count; } }, 0); + /** @type {string[]} */ this.optionalParameters = optionalParameters; + /** @type {Node[] | null | undefined} */ this.frames = frames; this.copyVisibilityInfo(visibilityInfo); this.allowRoot = true; + /** @type {boolean} */ this.evalFirst = true; } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.params && this.params.length) { - this.params = visitor.visitArray(this.params); + this.params = /** @type {MixinParam[]} */ (/** @type {unknown} */ (visitor.visitArray(/** @type {Node[]} */ (/** @type {unknown} */ (this.params))))); } this.rules = visitor.visitArray(this.rules); if (this.condition) { @@ -47,28 +86,43 @@ class Definition extends Ruleset { } } + /** + * @param {EvalContext} context + * @param {EvalContext} mixinEnv + * @param {MixinArg[] | null} args + * @param {Node[]} evaldArguments + * @returns {Ruleset} + */ evalParams(context, mixinEnv, args, evaldArguments) { /* jshint boss:true */ const frame = new Ruleset(null, null); + /** @type {Node[] | undefined} */ let varargs; + /** @type {MixinArg | undefined} */ let arg; - const params = utils.copyArray(this.params); + const params = /** @type {MixinParam[]} */ (utils.copyArray(this.params)); + /** @type {number} */ let i; + /** @type {number} */ let j; + /** @type {Node | undefined} */ let val; + /** @type {string | undefined} */ let name; + /** @type {boolean} */ let isNamedFound; + /** @type {number} */ let argIndex; let argsLength = 0; - if (mixinEnv.frames && mixinEnv.frames[0] && mixinEnv.frames[0].functionRegistry) { - frame.functionRegistry = mixinEnv.frames[0].functionRegistry.inherit(); + if (mixinEnv.frames && mixinEnv.frames[0] && /** @type {RulesetWithRegistry} */ (mixinEnv.frames[0]).functionRegistry) { + /** @type {RulesetWithRegistry} */ (frame).functionRegistry = /** @type {FunctionRegistry} */ (/** @type {RulesetWithRegistry} */ (mixinEnv.frames[0]).functionRegistry).inherit(); } - mixinEnv = new contexts.Eval(mixinEnv, [frame].concat(mixinEnv.frames)); + mixinEnv = new contexts.Eval(mixinEnv, /** @type {Node[]} */ ([frame]).concat(/** @type {Node[]} */ (mixinEnv.frames))); if (args) { - args = utils.copyArray(args); + args = /** @type {MixinArg[]} */ (utils.copyArray(args)); argsLength = args.length; for (i = 0; i < argsLength; i++) { @@ -103,7 +157,7 @@ class Definition extends Ruleset { if (params[i].variadic) { varargs = []; for (j = argIndex; j < argsLength; j++) { - varargs.push(args[j].value.eval(context)); + varargs.push(/** @type {MixinArg[]} */ (args)[j].value.eval(context)); } frame.prependRule(new Declaration(name, new Expression(varargs).eval(context))); } else { @@ -111,13 +165,13 @@ class Definition extends Ruleset { if (val) { // This was a mixin call, pass in a detached ruleset of it's eval'd rules if (Array.isArray(val)) { - val = new DetachedRuleset(new Ruleset('', val)); + val = /** @type {Node} */ (/** @type {unknown} */ (new DetachedRuleset(new Ruleset(null, /** @type {Node[]} */ (val))))); } else { val = val.eval(context); } } else if (params[i].value) { - val = params[i].value.eval(mixinEnv); + val = /** @type {Node} */ (params[i].value).eval(mixinEnv); frame.resetCache(); } else { throw { type: 'Runtime', message: `wrong number of arguments for ${this.name} (${argsLength} for ${this.arity})` }; @@ -139,8 +193,9 @@ class Definition extends Ruleset { return frame; } + /** @returns {Ruleset} */ makeImportant() { - const rules = !this.rules ? this.rules : this.rules.map(function (r) { + const rules = !this.rules ? this.rules : this.rules.map(function (/** @type {Node & { makeImportant?: (important?: boolean) => Node }} */ r) { if (r.makeImportant) { return r.makeImportant(true); } else { @@ -148,18 +203,31 @@ class Definition extends Ruleset { } }); const result = new Definition(this.name, this.params, rules, this.condition, this.variadic, this.frames); - return result; + return /** @type {Ruleset} */ (/** @type {unknown} */ (result)); } + /** + * @param {EvalContext} context + * @returns {Definition} + */ eval(context) { return new Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || utils.copyArray(context.frames)); } + /** + * @param {EvalContext} context + * @param {MixinArg[]} args + * @param {string | undefined} important + * @returns {Ruleset} + */ evalCall(context, args, important) { + /** @type {Node[]} */ const _arguments = []; - const mixinFrames = this.frames ? this.frames.concat(context.frames) : context.frames; + const mixinFrames = this.frames ? /** @type {Node[]} */ (this.frames).concat(/** @type {Node[]} */ (context.frames)) : /** @type {Node[]} */ (context.frames); const frame = this.evalParams(context, new contexts.Eval(context, mixinFrames), args, _arguments); + /** @type {Node[]} */ let rules; + /** @type {Ruleset} */ let ruleset; frame.prependRule(new Declaration('@arguments', new Expression(_arguments).eval(context))); @@ -167,31 +235,41 @@ class Definition extends Ruleset { rules = utils.copyArray(this.rules); ruleset = new Ruleset(null, rules); - ruleset.originalRuleset = this; - ruleset = ruleset.eval(new contexts.Eval(context, [this, frame].concat(mixinFrames))); + /** @type {RulesetWithRegistry} */ (ruleset).originalRuleset = this; + ruleset = /** @type {Ruleset} */ (ruleset.eval(new contexts.Eval(context, /** @type {Node[]} */ ([this, frame]).concat(mixinFrames)))); if (important) { - ruleset = ruleset.makeImportant(); + ruleset = /** @type {Ruleset} */ (ruleset.makeImportant()); } return ruleset; } + /** + * @param {MixinArg[] | null} args + * @param {EvalContext} context + * @returns {boolean} + */ matchCondition(args, context) { if (this.condition && !this.condition.eval( new contexts.Eval(context, - [this.evalParams(context, /* the parameter variables */ - new contexts.Eval(context, this.frames ? this.frames.concat(context.frames) : context.frames), args, [])] - .concat(this.frames || []) // the parent namespace/mixin frames - .concat(context.frames)))) { // the current environment frames + /** @type {Node[]} */ ([this.evalParams(context, /* the parameter variables */ + new contexts.Eval(context, this.frames ? /** @type {Node[]} */ (this.frames).concat(/** @type {Node[]} */ (context.frames)) : context.frames), args, [])]) + .concat(/** @type {Node[]} */ (this.frames || [])) // the parent namespace/mixin frames + .concat(/** @type {Node[]} */ (context.frames))))) { // the current environment frames return false; } return true; } + /** + * @param {MixinArg[] | null} args + * @param {EvalContext} [context] + * @returns {boolean} + */ matchArgs(args, context) { const allArgsCnt = (args && args.length) || 0; let len; const optionalParameters = this.optionalParameters; - const requiredArgsCnt = !args ? 0 : args.reduce(function (count, p) { + const requiredArgsCnt = !args ? 0 : args.reduce(function (/** @type {number} */ count, /** @type {MixinArg} */ p) { if (optionalParameters.indexOf(p.name) < 0) { return count + 1; } else { @@ -217,7 +295,7 @@ class Definition extends Ruleset { for (let i = 0; i < len; i++) { if (!this.params[i].name && !this.params[i].variadic) { - if (args[i].value.eval(context).toCSS() != this.params[i].value.eval(context).toCSS()) { + if (/** @type {MixinArg[]} */ (args)[i].value.eval(context).toCSS(/** @type {EvalContext} */ ({})) != /** @type {Node} */ (this.params[i].value).eval(context).toCSS(/** @type {EvalContext} */ ({}))) { return false; } } diff --git a/packages/less/lib/less/tree/namespace-value.js b/packages/less/lib/less/tree/namespace-value.js index b6704357c..bc583c9a3 100644 --- a/packages/less/lib/less/tree/namespace-value.js +++ b/packages/less/lib/less/tree/namespace-value.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, FileInfo } from './node.js' */ import Node from './node.js'; import Variable from './variable.js'; import Ruleset from './ruleset.js'; @@ -6,6 +8,12 @@ import Selector from './selector.js'; class NamespaceValue extends Node { get type() { return 'NamespaceValue'; } + /** + * @param {Node} ruleCall + * @param {string[]} lookups + * @param {number} index + * @param {FileInfo} fileInfo + */ constructor(ruleCall, lookups, index, fileInfo) { super(); this.value = ruleCall; @@ -14,8 +22,14 @@ class NamespaceValue extends Node { this._fileInfo = fileInfo; } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { - let i, name, rules = this.value.eval(context); + let i, name; + /** @type {Ruleset | Node | Node[]} */ + let rules = this.value.eval(context); for (i = 0; i < this.lookups.length; i++) { name = this.lookups[i]; @@ -24,15 +38,17 @@ class NamespaceValue extends Node { rules = new Ruleset([new Selector()], rules); } + const rs = /** @type {Ruleset} */ (rules); + if (name === '') { - rules = rules.lastDeclaration(); + rules = rs.lastDeclaration(); } else if (name.charAt(0) === '@') { if (name.charAt(1) === '@') { name = `@${new Variable(name.slice(1)).eval(context).value}`; } - if (rules.variables) { - rules = rules.variable(name); + if (rs.variables) { + rules = rs.variable(name); } if (!rules) { @@ -49,8 +65,8 @@ class NamespaceValue extends Node { else { name = name.charAt(0) === '$' ? name : `$${name}`; } - if (rules.properties) { - rules = rules.property(name); + if (rs.properties) { + rules = rs.property(name); } if (!rules) { @@ -59,17 +75,20 @@ class NamespaceValue extends Node { filename: this.fileInfo().filename, index: this.getIndex() }; } - rules = rules[rules.length - 1]; + const rulesArr = /** @type {Node[]} */ (rules); + rules = rulesArr[rulesArr.length - 1]; } - if (rules.value) { - rules = rules.eval(context).value; + const current = /** @type {Node} */ (rules); + if (current.value) { + rules = /** @type {Node} */ (current.eval(context).value); } - if (rules.ruleset) { - rules = rules.ruleset.eval(context); + const currentNode = /** @type {Node & { ruleset?: Node }} */ (rules); + if (currentNode.ruleset) { + rules = currentNode.ruleset.eval(context); } } - return rules; + return /** @type {Node} */ (rules); } } diff --git a/packages/less/lib/less/tree/negative.js b/packages/less/lib/less/tree/negative.js index cbf13ffc7..1d7437179 100644 --- a/packages/less/lib/less/tree/negative.js +++ b/packages/less/lib/less/tree/negative.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput } from './node.js' */ import Node from './node.js'; import Operation from './operation.js'; import Dimension from './dimension.js'; @@ -5,21 +7,30 @@ import Dimension from './dimension.js'; class Negative extends Node { get type() { return 'Negative'; } + /** @param {Node} node */ constructor(node) { super(); this.value = node; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add('-'); - this.value.genCSS(context, output); + /** @type {Node} */ (this.value).genCSS(context, output); } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { - if (context.isMathOn()) { - return (new Operation('*', [new Dimension(-1), this.value])).eval(context); + if (context.isMathOn('*')) { + return (new Operation('*', [new Dimension(-1), /** @type {Node} */ (this.value)], false)).eval(context); } - return new Negative(this.value.eval(context)); + return new Negative(/** @type {Node} */ (this.value).eval(context)); } } diff --git a/packages/less/lib/less/tree/nested-at-rule.js b/packages/less/lib/less/tree/nested-at-rule.js index 2358f3cc1..8d0c4d33b 100644 --- a/packages/less/lib/less/tree/nested-at-rule.js +++ b/packages/less/lib/less/tree/nested-at-rule.js @@ -1,9 +1,41 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */ import Ruleset from './ruleset.js'; import Value from './value.js'; import Selector from './selector.js'; import Anonymous from './anonymous.js'; import Expression from './expression.js'; import * as utils from '../utils.js'; +import Node from './node.js'; + +/** + * @typedef {object} FunctionRegistry + * @property {(name: string, func: Function) => void} add + * @property {(functions: Object) => void} addMultiple + * @property {(name: string) => Function} get + * @property {() => Object} getLocalFunctions + * @property {() => FunctionRegistry} inherit + * @property {(base: FunctionRegistry) => FunctionRegistry} create + */ + +/** + * @typedef {Node & { + * features: Value, + * rules: Ruleset[], + * type: string, + * functionRegistry?: FunctionRegistry, + * multiMedia?: boolean, + * debugInfo?: { lineNumber: number, fileName: string }, + * allowRoot?: boolean, + * _evaluated?: boolean, + * evalFunction: () => void, + * evalTop: (context: EvalContext) => Node | Ruleset, + * evalNested: (context: EvalContext) => Node | Ruleset, + * permute: (arr: Node[][]) => Node[][], + * bubbleSelectors: (selectors: Selector[] | undefined) => void, + * outputRuleset: (context: EvalContext, output: CSSOutput, rules: Node[]) => void + * }} NestableAtRuleThis + */ const NestableAtRulePrototype = { @@ -11,52 +43,64 @@ const NestableAtRulePrototype = { return true; }, + /** @param {TreeVisitor} visitor */ accept(visitor) { - if (this.features) { - this.features = visitor.visit(this.features); + /** @type {NestableAtRuleThis} */ + const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this)); + if (self.features) { + self.features = /** @type {Value} */ (visitor.visit(self.features)); } - if (this.rules) { - this.rules = visitor.visitArray(this.rules); + if (self.rules) { + self.rules = /** @type {Ruleset[]} */ (visitor.visitArray(self.rules)); } }, evalFunction: function () { - if (!this.features || !Array.isArray(this.features.value) || this.features.value.length < 1) { + /** @type {NestableAtRuleThis} */ + const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this)); + if (!self.features || !Array.isArray(self.features.value) || self.features.value.length < 1) { return; } - const exprValues = this.features.value; - let expr, paren; + const exprValues = /** @type {Node[]} */ (self.features.value); + /** @type {Node | undefined} */ + let expr; + /** @type {Node | undefined} */ + let paren; for (let index = 0; index < exprValues.length; ++index) { expr = exprValues[index]; if ((expr.type === 'Keyword' || expr.type === 'Variable') && index + 1 < exprValues.length - && (expr.noSpacing || expr.noSpacing == null)) { + && (/** @type {Node & { noSpacing?: boolean }} */ (expr).noSpacing || /** @type {Node & { noSpacing?: boolean }} */ (expr).noSpacing == null)) { paren = exprValues[index + 1]; - - if (paren.type === 'Paren' && paren.noSpacing) { + + if (paren.type === 'Paren' && /** @type {Node & { noSpacing?: boolean }} */ (paren).noSpacing) { exprValues[index]= new Expression([expr, paren]); exprValues.splice(index + 1, 1); - exprValues[index].noSpacing = true; + /** @type {Node & { noSpacing?: boolean }} */ (exprValues[index]).noSpacing = true; } } } }, + /** @param {EvalContext} context */ evalTop(context) { - this.evalFunction(); + /** @type {NestableAtRuleThis} */ + const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this)); + self.evalFunction(); - let result = this; + /** @type {Node | Ruleset} */ + let result = self; // Render all dependent Media blocks. if (context.mediaBlocks.length > 1) { - const selectors = (new Selector([], null, null, this.getIndex(), this.fileInfo())).createEmptySelectors(); + const selectors = (new Selector([], null, null, self.getIndex(), self.fileInfo())).createEmptySelectors(); result = new Ruleset(selectors, context.mediaBlocks); - result.multiMedia = true; - result.copyVisibilityInfo(this.visibilityInfo()); - this.setParent(result, this); + /** @type {Ruleset & { multiMedia?: boolean }} */ (result).multiMedia = true; + result.copyVisibilityInfo(self.visibilityInfo()); + self.setParent(result, self); } delete context.mediaBlocks; @@ -65,26 +109,30 @@ const NestableAtRulePrototype = { return result; }, + /** @param {EvalContext} context */ evalNested(context) { - this.evalFunction(); + /** @type {NestableAtRuleThis} */ + const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this)); + self.evalFunction(); let i; + /** @type {Node | Node[]} */ let value; - const path = context.mediaPath.concat([this]); + const path = context.mediaPath.concat([self]); // Extract the media-query conditions separated with `,` (OR). for (i = 0; i < path.length; i++) { - if (path[i].type !== this.type) { - const blockIndex = context.mediaBlocks.indexOf(this); + if (path[i].type !== self.type) { + const blockIndex = context.mediaBlocks.indexOf(self); if (blockIndex > -1) { context.mediaBlocks.splice(blockIndex, 1); } - return this; + return self; } - - value = path[i].features instanceof Value ? - path[i].features.value : path[i].features; - path[i] = Array.isArray(value) ? value : [value]; + + value = /** @type {NestableAtRuleThis} */ (path[i]).features instanceof Value ? + /** @type {Node[]} */ (/** @type {NestableAtRuleThis} */ (path[i]).features.value) : /** @type {NestableAtRuleThis} */ (path[i]).features; + path[i] = /** @type {Node} */ (/** @type {unknown} */ (Array.isArray(value) ? value : [value])); } // Trace all permutations to generate the resulting media-query. @@ -94,27 +142,36 @@ const NestableAtRulePrototype = { // a and e // b and c and d // b and c and e - this.features = new Value(this.permute(path).map(path => { - path = path.map(fragment => fragment.toCSS ? fragment : new Anonymous(fragment)); - - for (i = path.length - 1; i > 0; i--) { - path.splice(i, 0, new Anonymous('and')); + self.features = new Value(self.permute(/** @type {Node[][]} */ (/** @type {unknown} */ (path))).map( + /** @param {Node | Node[]} path */ + path => { + path = /** @type {Node[]} */ (path).map( + /** @param {Node & { toCSS?: Function }} fragment */ + fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment)))); + + for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) { + /** @type {Node[]} */ (path).splice(i, 0, new Anonymous('and')); } - return new Expression(path); + return new Expression(/** @type {Node[]} */ (path)); })); - this.setParent(this.features, this); + self.setParent(self.features, self); // Fake a tree-node that doesn't output anything. return new Ruleset([], []); }, + /** + * @param {Node[][]} arr + * @returns {Node[][]} + */ permute(arr) { if (arr.length === 0) { return []; } else if (arr.length === 1) { - return arr[0]; + return /** @type {Node[][]} */ (/** @type {unknown} */ (arr[0])); } else { + /** @type {Node[][]} */ const result = []; const rest = this.permute(arr.slice(1)); for (let i = 0; i < rest.length; i++) { @@ -126,12 +183,15 @@ const NestableAtRulePrototype = { } }, + /** @param {Selector[] | undefined} selectors */ bubbleSelectors(selectors) { + /** @type {NestableAtRuleThis} */ + const self = /** @type {NestableAtRuleThis} */ (/** @type {unknown} */ (this)); if (!selectors) { return; } - this.rules = [new Ruleset(utils.copyArray(selectors), [this.rules[0]])]; - this.setParent(this.rules, this); + self.rules = [new Ruleset(utils.copyArray(selectors), [self.rules[0]])]; + self.setParent(self.rules, self); } }; diff --git a/packages/less/lib/less/tree/node.js b/packages/less/lib/less/tree/node.js index 7ab16e889..2b5692af5 100644 --- a/packages/less/lib/less/tree/node.js +++ b/packages/less/lib/less/tree/node.js @@ -1,3 +1,4 @@ +// @ts-check /** * @typedef {object} FileInfo * @property {string} [filename] @@ -16,17 +17,47 @@ /** * @typedef {object} CSSOutput - * @property {(chunk: string, fileInfo?: FileInfo, index?: number) => void} add + * @property {(chunk: string, fileInfo?: FileInfo, index?: number, mapLines?: boolean) => void} add * @property {() => boolean} isEmpty */ /** * @typedef {object} EvalContext * @property {number} [numPrecision] - * @property {boolean} [isMathOn] - * @property {string} [math] - * @property {Array} [frames] - * @property {boolean} [importantScope] + * @property {(op?: string) => boolean} [isMathOn] + * @property {number} [math] + * @property {Node[]} [frames] + * @property {Array<{important?: string}>} [importantScope] + * @property {string[]} [paths] + * @property {boolean} [compress] + * @property {boolean} [strictUnits] + * @property {boolean} [sourceMap] + * @property {boolean} [importMultiple] + * @property {string} [urlArgs] + * @property {boolean} [javascriptEnabled] + * @property {object} [pluginManager] + * @property {number} [rewriteUrls] + * @property {boolean} [inCalc] + * @property {boolean} [mathOn] + * @property {boolean[]} [calcStack] + * @property {boolean[]} [parensStack] + * @property {Node[]} [mediaBlocks] + * @property {Node[]} [mediaPath] + * @property {() => void} [inParenthesis] + * @property {() => void} [outOfParenthesis] + * @property {() => void} [enterCalc] + * @property {() => void} [exitCalc] + * @property {(path: string) => boolean} [pathRequiresRewrite] + * @property {(path: string, rootpath?: string) => string} [rewritePath] + * @property {(path: string) => string} [normalizePath] + * @property {number} [tabLevel] + * @property {boolean} [lastRule] + */ + +/** + * @typedef {object} TreeVisitor + * @property {(node: Node) => Node} visit + * @property {(nodes: Node[], nonReplacing?: boolean) => Node[]} visitArray */ /** @@ -47,10 +78,10 @@ class Node { this.nodeVisible = undefined; /** @type {Node | null} */ this.rootNode = null; - /** @type {object | null} */ + /** @type {Node | null} */ this.parsed = null; - /** @type {*} */ + /** @type {Node | Node[] | string | number | undefined} */ this.value = undefined; /** @type {number | undefined} */ this._index = undefined; @@ -121,18 +152,18 @@ class Node { * @param {CSSOutput} output */ genCSS(context, output) { - output.add(this.value); + output.add(/** @type {string} */ (this.value)); } /** - * @param {{ visit: (node: *) => * }} visitor + * @param {TreeVisitor} visitor */ accept(visitor) { - this.value = visitor.visit(this.value); + this.value = visitor.visit(/** @type {Node} */ (this.value)); } /** - * @param {*} [context] + * @param {EvalContext} [context] * @returns {Node} */ eval(context) { return this; } @@ -187,13 +218,14 @@ class Node { return undefined; } - /** @type {*} */ let aVal = a.value; - /** @type {*} */ let bVal = b.value; if (!Array.isArray(aVal)) { return aVal === bVal ? 0 : undefined; } + if (!Array.isArray(bVal)) { + return undefined; + } if (aVal.length !== bVal.length) { return undefined; } @@ -206,8 +238,8 @@ class Node { } /** - * @param {number} a - * @param {number} b + * @param {number | string} a + * @param {number | string} b * @returns {number | undefined} */ static numericCompare(a, b) { @@ -269,4 +301,10 @@ class Node { } } +/** + * Set by the parser at runtime on Node.prototype. + * @type {{ context: EvalContext, importManager: object, imports: object } | undefined} + */ +Node.prototype.parse = undefined; + export default Node; diff --git a/packages/less/lib/less/tree/operation.js b/packages/less/lib/less/tree/operation.js index e0fc244ba..02c597264 100644 --- a/packages/less/lib/less/tree/operation.js +++ b/packages/less/lib/less/tree/operation.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */ import Node from './node.js'; import Color from './color.js'; import Dimension from './dimension.js'; @@ -7,6 +9,11 @@ const MATH = Constants.Math; class Operation extends Node { get type() { return 'Operation'; } + /** + * @param {string} op + * @param {Node[]} operands + * @param {boolean} isSpaced + */ constructor(op, operands, isSpaced) { super(); this.op = op.trim(); @@ -14,25 +21,30 @@ class Operation extends Node { this.isSpaced = isSpaced; } + /** @param {TreeVisitor} visitor */ accept(visitor) { this.operands = visitor.visitArray(this.operands); } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { let a = this.operands[0].eval(context), b = this.operands[1].eval(context), op; if (context.isMathOn(this.op)) { op = this.op === './' ? '/' : this.op; if (a instanceof Dimension && b instanceof Color) { - a = a.toColor(); + a = /** @type {Dimension} */ (a).toColor(); } if (b instanceof Dimension && a instanceof Color) { - b = b.toColor(); + b = /** @type {Dimension} */ (b).toColor(); } - if (!a.operate || !b.operate) { + if (!/** @type {Dimension | Color} */ (a).operate || !/** @type {Dimension | Color} */ (b).operate) { if ( (a instanceof Operation || b instanceof Operation) - && a.op === '/' && context.math === MATH.PARENS_DIVISION + && /** @type {Operation} */ (a).op === '/' && context.math === MATH.PARENS_DIVISION ) { return new Operation(this.op, [a, b], this.isSpaced); } @@ -40,12 +52,19 @@ class Operation extends Node { message: 'Operation on an invalid type' }; } - return a.operate(context, op, b); + if (a instanceof Dimension) { + return a.operate(context, op, /** @type {Dimension} */ (b)); + } + return /** @type {Color} */ (a).operate(context, op, /** @type {Color} */ (b)); } else { return new Operation(this.op, [a, b], this.isSpaced); } } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { this.operands[0].genCSS(context, output); if (this.isSpaced) { diff --git a/packages/less/lib/less/tree/paren.js b/packages/less/lib/less/tree/paren.js index 5976ad49e..05bbc369a 100644 --- a/packages/less/lib/less/tree/paren.js +++ b/packages/less/lib/less/tree/paren.js @@ -1,21 +1,34 @@ +// @ts-check +/** @import { EvalContext, CSSOutput } from './node.js' */ import Node from './node.js'; class Paren extends Node { get type() { return 'Paren'; } + /** @param {Node} node */ constructor(node) { super(); this.value = node; + /** @type {boolean | undefined} */ + this.noSpacing = undefined; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add('('); - this.value.genCSS(context, output); + /** @type {Node} */ (this.value).genCSS(context, output); output.add(')'); } + /** + * @param {EvalContext} context + * @returns {Paren} + */ eval(context) { - const paren = new Paren(this.value.eval(context)); + const paren = new Paren(/** @type {Node} */ (this.value).eval(context)); if (this.noSpacing) { paren.noSpacing = true; diff --git a/packages/less/lib/less/tree/property.js b/packages/less/lib/less/tree/property.js index 481aadf5a..08342477d 100644 --- a/packages/less/lib/less/tree/property.js +++ b/packages/less/lib/less/tree/property.js @@ -1,21 +1,35 @@ +// @ts-check +/** @import { EvalContext, FileInfo } from './node.js' */ import Node from './node.js'; import Declaration from './declaration.js'; +import Ruleset from './ruleset.js'; class Property extends Node { get type() { return 'Property'; } + /** + * @param {string} name + * @param {number} index + * @param {FileInfo} currentFileInfo + */ constructor(name, index, currentFileInfo) { super(); this.name = name; this._index = index; this._fileInfo = currentFileInfo; + /** @type {boolean | undefined} */ + this.evaluating = undefined; } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { let property; const name = this.name; // TODO: shorten this reference - const mergeRules = context.pluginManager.less.visitors.ToCSSVisitor.prototype._mergeRules; + const mergeRules = /** @type {{ less: { visitors: { ToCSSVisitor: { prototype: { _mergeRules: (rules: Declaration[]) => void } } } } }} */ (context.pluginManager).less.visitors.ToCSSVisitor.prototype._mergeRules; if (this.evaluating) { throw { type: 'Name', @@ -26,9 +40,9 @@ class Property extends Node { this.evaluating = true; - property = this.find(context.frames, function (frame) { + property = this.find(context.frames, function (/** @type {Node} */ frame) { let v; - const vArr = frame.property(name); + const vArr = /** @type {Ruleset} */ (frame).property(name); if (vArr) { for (let i = 0; i < vArr.length; i++) { v = vArr[i]; @@ -65,6 +79,11 @@ class Property extends Node { } } + /** + * @param {Node[]} obj + * @param {(frame: Node) => Node | undefined} fun + * @returns {Node | null} + */ find(obj, fun) { for (let i = 0, r; i < obj.length; i++) { r = fun.call(obj, obj[i]); diff --git a/packages/less/lib/less/tree/query-in-parens.js b/packages/less/lib/less/tree/query-in-parens.js index 84938b616..fd201a4e8 100644 --- a/packages/less/lib/less/tree/query-in-parens.js +++ b/packages/less/lib/less/tree/query-in-parens.js @@ -1,18 +1,30 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */ import Node from './node.js'; class QueryInParens extends Node { get type() { return 'QueryInParens'; } + /** + * @param {string} op + * @param {Node} l + * @param {Node} m + * @param {string | null} op2 + * @param {Node | null} r + * @param {number} i + */ constructor(op, l, m, op2, r, i) { super(); this.op = op.trim(); this.lvalue = l; this.mvalue = m; this.op2 = op2 ? op2.trim() : null; + /** @type {Node | null} */ this.rvalue = r; this._index = i; } + /** @param {TreeVisitor} visitor */ accept(visitor) { this.lvalue = visitor.visit(this.lvalue); this.mvalue = visitor.visit(this.mvalue); @@ -21,6 +33,7 @@ class QueryInParens extends Node { } } + /** @param {EvalContext} context */ eval(context) { const node = new QueryInParens( this.op, @@ -28,11 +41,15 @@ class QueryInParens extends Node { this.mvalue.eval(context), this.op2, this.rvalue ? this.rvalue.eval(context) : null, - this._index + this._index || 0 ); return node; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { this.lvalue.genCSS(context, output); output.add(' ' + this.op + ' '); diff --git a/packages/less/lib/less/tree/quoted.js b/packages/less/lib/less/tree/quoted.js index 64c619dcc..9f786a810 100644 --- a/packages/less/lib/less/tree/quoted.js +++ b/packages/less/lib/less/tree/quoted.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, FileInfo } from './node.js' */ import Node from './node.js'; import Variable from './variable.js'; import Property from './property.js'; @@ -5,43 +7,80 @@ import Property from './property.js'; class Quoted extends Node { get type() { return 'Quoted'; } + /** + * @param {string} str + * @param {string} [content] + * @param {boolean} [escaped] + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + */ constructor(str, content, escaped, index, currentFileInfo) { super(); + /** @type {boolean} */ this.escaped = (escaped === undefined) ? true : escaped; + /** @type {string} */ this.value = content || ''; + /** @type {string} */ this.quote = str.charAt(0); this._index = index; this._fileInfo = currentFileInfo; + /** @type {RegExp} */ this.variableRegex = /@\{([\w-]+)\}/g; + /** @type {RegExp} */ this.propRegex = /\$\{([\w-]+)\}/g; + /** @type {boolean | undefined} */ this.allowRoot = escaped; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { if (!this.escaped) { output.add(this.quote, this.fileInfo(), this.getIndex()); } - output.add(this.value); + output.add(/** @type {string} */ (this.value)); if (!this.escaped) { output.add(this.quote); } } + /** @returns {RegExpMatchArray | null} */ containsVariables() { - return this.value.match(this.variableRegex); + return /** @type {string} */ (this.value).match(this.variableRegex); } + /** @param {EvalContext} context */ eval(context) { const that = this; - let value = this.value; + let value = /** @type {string} */ (this.value); + /** + * @param {string} _ + * @param {string} name1 + * @param {string} name2 + * @returns {string} + */ const variableReplacement = function (_, name1, name2) { - const v = new Variable(`@${name1 ?? name2}`, that.getIndex(), that.fileInfo()).eval(context, true); - return (v instanceof Quoted) ? v.value : v.toCSS(); + const v = new Variable(`@${name1 ?? name2}`, that.getIndex(), that.fileInfo()).eval(context); + return (v instanceof Quoted) ? /** @type {string} */ (v.value) : v.toCSS(context); }; + /** + * @param {string} _ + * @param {string} name1 + * @param {string} name2 + * @returns {string} + */ const propertyReplacement = function (_, name1, name2) { - const v = new Property(`$${name1 ?? name2}`, that.getIndex(), that.fileInfo()).eval(context, true); - return (v instanceof Quoted) ? v.value : v.toCSS(); + const v = new Property(`$${name1 ?? name2}`, that.getIndex(), that.fileInfo()).eval(context); + return (v instanceof Quoted) ? /** @type {string} */ (v.value) : v.toCSS(context); }; + /** + * @param {string} value + * @param {RegExp} regexp + * @param {(substring: string, ...args: string[]) => string} replacementFnc + * @returns {string} + */ function iterativeReplace(value, regexp, replacementFnc) { let evaluatedValue = value; do { @@ -55,12 +94,19 @@ class Quoted extends Node { return new Quoted(this.quote + value + this.quote, value, this.escaped, this.getIndex(), this.fileInfo()); } + /** + * @param {Node} other + * @returns {number | undefined} + */ compare(other) { // when comparing quoted strings allow the quote to differ - if (other.type === 'Quoted' && !this.escaped && !other.escaped) { - return Node.numericCompare(this.value, other.value); + if (other.type === 'Quoted' && !this.escaped && !/** @type {Quoted} */ (other).escaped) { + return Node.numericCompare( + /** @type {string} */ (this.value), + /** @type {string} */ (other.value) + ); } else { - return other.toCSS && this.toCSS() === other.toCSS() ? 0 : undefined; + return other.toCSS && this.toCSS(/** @type {EvalContext} */ ({})) === other.toCSS(/** @type {EvalContext} */ ({})) ? 0 : undefined; } } } diff --git a/packages/less/lib/less/tree/ruleset.js b/packages/less/lib/less/tree/ruleset.js index 1bd256715..ae780ad31 100644 --- a/packages/less/lib/less/tree/ruleset.js +++ b/packages/less/lib/less/tree/ruleset.js @@ -1,3 +1,6 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */ +/** @import { FunctionRegistry } from './nested-at-rule.js' */ import Node from './node.js'; import Declaration from './declaration.js'; import Keyword from './keyword.js'; @@ -13,43 +16,101 @@ import getDebugInfo from './debug-info.js'; import * as utils from '../utils.js'; import Parser from '../parser/parser.js'; +/** + * @typedef {Node & { + * rules?: Node[], + * selectors?: Selector[], + * root?: boolean, + * firstRoot?: boolean, + * allowImports?: boolean, + * functionRegistry?: FunctionRegistry, + * originalRuleset?: Node, + * debugInfo?: { lineNumber: number, fileName: string }, + * evalFirst?: boolean, + * isRuleset?: boolean, + * isCharset?: () => boolean, + * merge?: boolean | string, + * multiMedia?: boolean, + * parse?: { context: EvalContext, importManager: object }, + * bubbleSelectors?: (selectors: Selector[]) => void + * }} RuleNode + */ + class Ruleset extends Node { get type() { return 'Ruleset'; } + /** + * @param {Selector[] | null} selectors + * @param {Node[] | null} rules + * @param {boolean} [strictImports] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(selectors, rules, strictImports, visibilityInfo) { super(); + /** @type {Selector[] | null} */ this.selectors = selectors; + /** @type {Node[] | null} */ this.rules = rules; + /** @type {Object} */ this._lookups = {}; + /** @type {Object | null} */ this._variables = null; + /** @type {Object | null} */ this._properties = null; + /** @type {boolean | undefined} */ this.strictImports = strictImports; this.copyVisibilityInfo(visibilityInfo); this.allowRoot = true; + /** @type {boolean} */ this.isRuleset = true; + /** @type {boolean | undefined} */ + this.root = undefined; + /** @type {boolean | undefined} */ + this.firstRoot = undefined; + /** @type {boolean | undefined} */ + this.allowImports = undefined; + /** @type {FunctionRegistry | undefined} */ + this.functionRegistry = undefined; + /** @type {Node | undefined} */ + this.originalRuleset = undefined; + /** @type {{ lineNumber: number, fileName: string } | undefined} */ + this.debugInfo = undefined; + /** @type {Selector[][] | undefined} */ + this.paths = undefined; + /** @type {Ruleset[] | null | undefined} */ + this._rulesets = undefined; + /** @type {boolean | undefined} */ + this.evalFirst = undefined; this.setParent(this.selectors, this); this.setParent(this.rules, this); } isRulesetLike() { return true; } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.paths) { - this.paths = visitor.visitArray(this.paths, true); + this.paths = /** @type {Selector[][]} */ (/** @type {unknown} */ (visitor.visitArray(/** @type {Node[]} */ (/** @type {unknown} */ (this.paths)), true))); } else if (this.selectors) { - this.selectors = visitor.visitArray(this.selectors); + this.selectors = /** @type {Selector[]} */ (visitor.visitArray(this.selectors)); } if (this.rules && this.rules.length) { this.rules = visitor.visitArray(this.rules); } } + /** @param {EvalContext} context */ eval(context) { + /** @type {Selector[] | undefined} */ let selectors; + /** @type {number} */ let selCnt; + /** @type {Selector} */ let selector; + /** @type {number} */ let i; + /** @type {boolean | undefined} */ let hasVariable; let hasOnePassingSelector = false; @@ -61,7 +122,7 @@ class Ruleset extends Node { }); for (i = 0; i < selCnt; i++) { - selector = this.selectors[i].eval(context); + selector = /** @type {Selector} */ (this.selectors[i].eval(context)); for (let j = 0; j < selector.elements.length; j++) { if (selector.elements[j].isVariable) { hasVariable = true; @@ -82,12 +143,12 @@ class Ruleset extends Node { } const startingIndex = selectors[0].getIndex(); const selectorFileInfo = selectors[0].fileInfo(); - new Parser(context, this.parse.importManager, selectorFileInfo, startingIndex).parseNode( + new (/** @type {new (...args: [EvalContext, object, FileInfo, number]) => { parseNode: Function }} */ (/** @type {unknown} */ (Parser)))(context, /** @type {{ context: EvalContext, importManager: object }} */ (this.parse).importManager, selectorFileInfo, startingIndex).parseNode( toParseSelectors.join(','), ['selectors'], - function(err, result) { + function(/** @type {Error | null} */ err, /** @type {Node[]} */ result) { if (result) { - selectors = utils.flattenArray(result); + selectors = /** @type {Selector[]} */ (utils.flattenArray(result)); } }); } @@ -99,7 +160,9 @@ class Ruleset extends Node { let rules = this.rules ? utils.copyArray(this.rules) : null; const ruleset = new Ruleset(selectors, rules, this.strictImports, this.visibilityInfo()); + /** @type {Node} */ let rule; + /** @type {Node} */ let subRule; ruleset.originalRuleset = this; @@ -112,7 +175,7 @@ class Ruleset extends Node { } if (!hasOnePassingSelector) { - rules.length = 0; + /** @type {Node[]} */ (rules).length = 0; } // push the current ruleset to the frames stack @@ -120,18 +183,20 @@ class Ruleset extends Node { // inherit a function registry from the frames stack when possible; // otherwise from the global registry + /** @type {FunctionRegistry | undefined} */ let foundRegistry; for (let fi = 0, fn = ctxFrames.length; fi !== fn; ++fi) { - foundRegistry = ctxFrames[fi].functionRegistry; + foundRegistry = /** @type {RuleNode} */ (ctxFrames[fi]).functionRegistry; if (foundRegistry) { break; } } ruleset.functionRegistry = (foundRegistry || globalFunctionRegistry).inherit(); ctxFrames.unshift(ruleset); // currrent selectors - let ctxSelectors = context.selectors; + /** @type {Selector[][] | undefined} */ + let ctxSelectors = /** @type {EvalContext & { selectors?: Selector[][] }} */ (context).selectors; if (!ctxSelectors) { - context.selectors = ctxSelectors = []; + /** @type {EvalContext & { selectors?: Selector[][] }} */ (context).selectors = ctxSelectors = []; } ctxSelectors.unshift(this.selectors); @@ -142,9 +207,9 @@ class Ruleset extends Node { // Store the frames around mixin definitions, // so they can be evaluated like closures when the time comes. - const rsRules = ruleset.rules; + const rsRules = /** @type {Node[]} */ (ruleset.rules); for (i = 0; (rule = rsRules[i]); i++) { - if (rule.evalFirst) { + if (/** @type {RuleNode} */ (rule).evalFirst) { rsRules[i] = rule.eval(context); } } @@ -155,28 +220,28 @@ class Ruleset extends Node { for (i = 0; (rule = rsRules[i]); i++) { if (rule.type === 'MixinCall') { /* jshint loopfunc:true */ - rules = rule.eval(context).filter(function(r) { + rules = /** @type {Node[]} */ (/** @type {unknown} */ (rule.eval(context))).filter(function(/** @type {Node & { variable?: boolean }} */ r) { if ((r instanceof Declaration) && r.variable) { // do not pollute the scope if the variable is // already there. consider returning false here // but we need a way to "return" variable from mixins - return !(ruleset.variable(r.name)); + return !(ruleset.variable(/** @type {string} */ (r.name))); } return true; }); - rsRules.splice.apply(rsRules, [i, 1].concat(rules)); + rsRules.splice.apply(rsRules, /** @type {[number, number, ...Node[]]} */ ([i, 1].concat(rules))); i += rules.length - 1; ruleset.resetCache(); } else if (rule.type === 'VariableCall') { /* jshint loopfunc:true */ - rules = rule.eval(context).rules.filter(function(r) { + rules = /** @type {Node[]} */ (/** @type {RuleNode} */ (rule.eval(context)).rules).filter(function(/** @type {Node & { variable?: boolean }} */ r) { if ((r instanceof Declaration) && r.variable) { // do not pollute the scope at all return false; } return true; }); - rsRules.splice.apply(rsRules, [i, 1].concat(rules)); + rsRules.splice.apply(rsRules, /** @type {[number, number, ...Node[]]} */ ([i, 1].concat(rules))); i += rules.length - 1; ruleset.resetCache(); } @@ -184,7 +249,7 @@ class Ruleset extends Node { // Evaluate everything else for (i = 0; (rule = rsRules[i]); i++) { - if (!rule.evalFirst) { + if (!/** @type {RuleNode} */ (rule).evalFirst) { rsRules[i] = rule = rule.eval ? rule.eval(context) : rule; } } @@ -215,25 +280,29 @@ class Ruleset extends Node { if (context.mediaBlocks) { for (i = mediaBlockCount; i < context.mediaBlocks.length; i++) { - context.mediaBlocks[i].bubbleSelectors(selectors); + /** @type {RuleNode} */ (context.mediaBlocks[i]).bubbleSelectors(selectors); } } return ruleset; } + /** @param {EvalContext} context */ evalImports(context) { const rules = this.rules; + /** @type {number} */ let i; + /** @type {Node | Node[]} */ let importRules; if (!rules) { return; } for (i = 0; i < rules.length; i++) { if (rules[i].type === 'Import') { importRules = rules[i].eval(context); - if (importRules && (importRules.length || importRules.length === 0)) { - rules.splice.apply(rules, [i, 1].concat(importRules)); - i += importRules.length - 1; + if (importRules && (/** @type {Node[]} */ (/** @type {unknown} */ (importRules)).length || /** @type {Node[]} */ (/** @type {unknown} */ (importRules)).length === 0)) { + const importArr = /** @type {Node[]} */ (/** @type {unknown} */ (importRules)); + rules.splice(i, 1, ...importArr); + i += importArr.length - 1; } else { rules.splice(i, 1, importRules); } @@ -243,7 +312,7 @@ class Ruleset extends Node { } makeImportant() { - const result = new Ruleset(this.selectors, this.rules.map(function (r) { + const result = new Ruleset(this.selectors, /** @type {Node[]} */ (this.rules).map(function (/** @type {Node & { makeImportant?: () => Node }} */ r) { if (r.makeImportant) { return r.makeImportant(); } else { @@ -254,13 +323,17 @@ class Ruleset extends Node { return result; } + /** @param {Node[] | object[] | null} [args] */ matchArgs(args) { return !args || args.length === 0; } - // lets you call a css selector with a guard + /** + * @param {Node[] | object[] | null} args + * @param {EvalContext} context + */ matchCondition(args, context) { - const lastSelector = this.selectors[this.selectors.length - 1]; + const lastSelector = /** @type {Selector[]} */ (this.selectors)[/** @type {Selector[]} */ (this.selectors).length - 1]; if (!lastSelector.evaldCondition) { return false; } @@ -282,18 +355,18 @@ class Ruleset extends Node { variables() { if (!this._variables) { - this._variables = !this.rules ? {} : this.rules.reduce(function (hash, r) { + this._variables = !this.rules ? {} : this.rules.reduce(function (/** @type {Object} */ hash, /** @type {Node} */ r) { if (r instanceof Declaration && r.variable === true) { - hash[r.name] = r; + hash[/** @type {string} */ (r.name)] = r; } // when evaluating variables in an import statement, imports have not been eval'd // so we need to go inside import statements. // guard against root being a string (in the case of inlined less) - if (r.type === 'Import' && r.root && r.root.variables) { - const vars = r.root.variables(); + if (r.type === 'Import' && /** @type {RuleNode} */ (r).root && /** @type {RuleNode & { root: Ruleset }} */ (r).root.variables) { + const vars = /** @type {RuleNode & { root: Ruleset }} */ (r).root.variables(); for (const name in vars) { if (Object.prototype.hasOwnProperty.call(vars, name)) { - hash[name] = r.root.variable(name); + hash[name] = /** @type {Declaration} */ (/** @type {RuleNode & { root: Ruleset }} */ (r).root.variable(name)); } } } @@ -305,10 +378,10 @@ class Ruleset extends Node { properties() { if (!this._properties) { - this._properties = !this.rules ? {} : this.rules.reduce(function (hash, r) { + this._properties = !this.rules ? {} : this.rules.reduce(function (/** @type {Object} */ hash, /** @type {Node} */ r) { if (r instanceof Declaration && r.variable !== true) { - const name = (r.name.length === 1) && (r.name[0] instanceof Keyword) ? - r.name[0].value : r.name; + const name = (/** @type {Node[]} */ (r.name).length === 1) && (/** @type {Node[]} */ (r.name)[0] instanceof Keyword) ? + /** @type {string} */ (/** @type {Node[]} */ (r.name)[0].value) : /** @type {string} */ (r.name); // Properties don't overwrite as they can merge if (!hash[`$${name}`]) { hash[`$${name}`] = [ r ]; @@ -323,6 +396,7 @@ class Ruleset extends Node { return this._properties; } + /** @param {string} name */ variable(name) { const decl = this.variables()[name]; if (decl) { @@ -330,6 +404,7 @@ class Ruleset extends Node { } } + /** @param {string} name */ property(name) { const decl = this.properties()[name]; if (decl) { @@ -338,34 +413,36 @@ class Ruleset extends Node { } lastDeclaration() { - for (let i = this.rules.length; i > 0; i--) { - const decl = this.rules[i - 1]; + for (let i = /** @type {Node[]} */ (this.rules).length; i > 0; i--) { + const decl = /** @type {Node[]} */ (this.rules)[i - 1]; if (decl instanceof Declaration) { return this.parseValue(decl); } } } + /** @param {Declaration | Declaration[]} toParse */ parseValue(toParse) { const self = this; + /** @param {Declaration} decl */ function transformDeclaration(decl) { - if (decl.value instanceof Anonymous && !decl.parsed) { + if (decl.value instanceof Anonymous && !/** @type {Declaration & { parsed?: boolean }} */ (decl).parsed) { if (typeof decl.value.value === 'string') { - new Parser(this.parse.context, this.parse.importManager, decl.fileInfo(), decl.value.getIndex()).parseNode( + new (/** @type {new (...args: [EvalContext, object, FileInfo, number]) => { parseNode: Function }} */ (/** @type {unknown} */ (Parser)))(/** @type {{ context: EvalContext, importManager: object }} */ (/** @type {Ruleset} */ (this).parse).context, /** @type {{ context: EvalContext, importManager: object }} */ (/** @type {Ruleset} */ (this).parse).importManager, decl.fileInfo(), decl.value.getIndex()).parseNode( decl.value.value, ['value', 'important'], - function(err, result) { + function(/** @type {Error | null} */ err, /** @type {Node[]} */ result) { if (err) { - decl.parsed = true; + decl.parsed = /** @type {Node} */ (/** @type {unknown} */ (true)); } if (result) { decl.value = result[0]; - decl.important = result[1] || ''; - decl.parsed = true; + /** @type {Declaration & { important?: string }} */ (decl).important = /** @type {string} */ (/** @type {unknown} */ (result[1])) || ''; + decl.parsed = /** @type {Node} */ (/** @type {unknown} */ (true)); } }); } else { - decl.parsed = true; + decl.parsed = /** @type {Node} */ (/** @type {unknown} */ (true)); } return decl; @@ -378,6 +455,7 @@ class Ruleset extends Node { return transformDeclaration.call(self, toParse); } else { + /** @type {Declaration[]} */ const nodes = []; for (let ti = 0; ti < toParse.length; ti++) { nodes.push(transformDeclaration.call(self, toParse[ti])); @@ -389,13 +467,16 @@ class Ruleset extends Node { rulesets() { if (!this.rules) { return []; } + /** @type {Node[]} */ const filtRules = []; const rules = this.rules; + /** @type {number} */ let i; + /** @type {Node} */ let rule; for (i = 0; (rule = rules[i]); i++) { - if (rule.isRuleset) { + if (/** @type {RuleNode} */ (rule).isRuleset) { filtRules.push(rule); } } @@ -403,6 +484,7 @@ class Ruleset extends Node { return filtRules; } + /** @param {Node} rule */ prependRule(rule) { const rules = this.rules; if (rules) { @@ -413,23 +495,32 @@ class Ruleset extends Node { this.setParent(rule, this); } + /** + * @param {Selector} selector + * @param {Ruleset | null} [self] + * @param {((rule: Node) => boolean)} [filter] + * @returns {{ rule: Node, path: Node[] }[]} + */ find(selector, self, filter) { self = self || this; + /** @type {{ rule: Node, path: Node[] }[]} */ const rules = []; + /** @type {number | undefined} */ let match; + /** @type {{ rule: Node, path: Node[] }[]} */ let foundMixins; - const key = selector.toCSS(); + const key = selector.toCSS(/** @type {EvalContext} */ ({})); - if (key in this._lookups) { return this._lookups[key]; } + if (key in this._lookups) { return /** @type {{ rule: Node, path: Node[] }[]} */ (this._lookups[key]); } this.rulesets().forEach(function (rule) { if (rule !== self) { - for (let j = 0; j < rule.selectors.length; j++) { - match = selector.match(rule.selectors[j]); + for (let j = 0; j < /** @type {RuleNode} */ (rule).selectors.length; j++) { + match = selector.match(/** @type {RuleNode} */ (rule).selectors[j]); if (match) { if (selector.elements.length > match) { if (!filter || filter(rule)) { - foundMixins = rule.find(new Selector(selector.elements.slice(match)), self, filter); + foundMixins = /** @type {Ruleset} */ (/** @type {unknown} */ (rule)).find(new Selector(selector.elements.slice(match)), self, filter); for (let i = 0; i < foundMixins.length; ++i) { foundMixins[i].path.push(rule); } @@ -447,16 +538,26 @@ class Ruleset extends Node { return rules; } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { + /** @type {number} */ let i; + /** @type {number} */ let j; + /** @type {Node[]} */ const charsetRuleNodes = []; + /** @type {Node[]} */ let ruleNodes = []; let // Line number debugging debugInfo; + /** @type {Node} */ let rule; + /** @type {Selector[]} */ let path; context.tabLevel = (context.tabLevel || 0); @@ -467,17 +568,18 @@ class Ruleset extends Node { const tabRuleStr = context.compress ? '' : Array(context.tabLevel + 1).join(' '); const tabSetStr = context.compress ? '' : Array(context.tabLevel).join(' '); + /** @type {string} */ let sep; let charsetNodeIndex = 0; let importNodeIndex = 0; - for (i = 0; (rule = this.rules[i]); i++) { + for (i = 0; (rule = /** @type {Node[]} */ (this.rules)[i]); i++) { if (rule instanceof Comment) { if (importNodeIndex === i) { importNodeIndex++; } ruleNodes.push(rule); - } else if (rule.isCharset && rule.isCharset()) { + } else if (/** @type {RuleNode} */ (rule).isCharset && /** @type {RuleNode} */ (rule).isCharset()) { ruleNodes.splice(charsetNodeIndex, 0, rule); charsetNodeIndex++; importNodeIndex++; @@ -493,15 +595,16 @@ class Ruleset extends Node { // If this is the root node, we don't render // a selector, or {}. if (!this.root) { - debugInfo = getDebugInfo(context, this, tabSetStr); + debugInfo = getDebugInfo(context, /** @type {{ debugInfo: { lineNumber: number, fileName: string } }} */ (/** @type {unknown} */ (this)), tabSetStr); if (debugInfo) { output.add(debugInfo); output.add(tabSetStr); } - const paths = this.paths; + const paths = /** @type {Selector[][]} */ (this.paths); const pathCnt = paths.length; + /** @type {number} */ let pathSubCnt; sep = context.compress ? ',' : (`,\n${tabSetStr}`); @@ -511,10 +614,10 @@ class Ruleset extends Node { if (!(pathSubCnt = path.length)) { continue; } if (i > 0) { output.add(sep); } - context.firstSelector = true; + /** @type {EvalContext & { firstSelector?: boolean }} */ (context).firstSelector = true; path[0].genCSS(context, output); - context.firstSelector = false; + /** @type {EvalContext & { firstSelector?: boolean }} */ (context).firstSelector = false; for (j = 1; j < pathSubCnt; j++) { path[j].genCSS(context, output); } @@ -531,14 +634,14 @@ class Ruleset extends Node { } const currentLastRule = context.lastRule; - if (rule.isRulesetLike(rule)) { + if (rule.isRulesetLike()) { context.lastRule = false; } if (rule.genCSS) { rule.genCSS(context, output); } else if (rule.value) { - output.add(rule.value.toString()); + output.add(/** @type {string} */ (rule.value).toString()); } context.lastRule = currentLastRule; @@ -560,16 +663,34 @@ class Ruleset extends Node { } } + /** + * @param {Selector[][]} paths + * @param {Selector[][]} context + * @param {Selector[]} selectors + */ joinSelectors(paths, context, selectors) { for (let s = 0; s < selectors.length; s++) { this.joinSelector(paths, context, selectors[s]); } } + /** + * @param {Selector[][]} paths + * @param {Selector[][]} context + * @param {Selector} selector + */ joinSelector(paths, context, selector) { + /** + * @param {Selector[]} elementsToPak + * @param {Element} originalElement + * @returns {Paren} + */ function createParenthesis(elementsToPak, originalElement) { - let replacementParen, j; + /** @type {Paren} */ + let replacementParen; + /** @type {number} */ + let j; if (elementsToPak.length === 0) { replacementParen = new Paren(elementsToPak[0]); } else { @@ -588,18 +709,35 @@ class Ruleset extends Node { return replacementParen; } + /** + * @param {Paren | Selector} containedElement + * @param {Element} originalElement + * @returns {Selector} + */ function createSelector(containedElement, originalElement) { - let element, selector; + /** @type {Element} */ + let element; + /** @type {Selector} */ + let selector; element = new Element(null, containedElement, originalElement.isVariable, originalElement._index, originalElement._fileInfo); selector = new Selector([element]); return selector; } - // joins selector path from `beginningPath` with selector path in `addPath` - // `replacedElement` contains element that is being replaced by `addPath` - // returns concatenated path + /** + * @param {Selector[]} beginningPath + * @param {Selector[]} addPath + * @param {Element} replacedElement + * @param {Selector} originalSelector + * @returns {Selector[]} + */ function addReplacementIntoPath(beginningPath, addPath, replacedElement, originalSelector) { - let newSelectorPath, lastSelector, newJoinedSelector; + /** @type {Selector[]} */ + let newSelectorPath; + /** @type {Selector} */ + let lastSelector; + /** @type {Selector} */ + let newJoinedSelector; // our new selector path newSelectorPath = []; @@ -645,7 +783,7 @@ class Ruleset extends Node { // put together the parent selectors after the join (e.g. the rest of the parent) if (addPath.length > 1) { let restOfPath = addPath.slice(1); - restOfPath = restOfPath.map(function (selector) { + restOfPath = restOfPath.map(function (/** @type {Selector} */ selector) { return selector.createDerived(selector.elements, []); }); newSelectorPath = newSelectorPath.concat(restOfPath); @@ -653,10 +791,16 @@ class Ruleset extends Node { return newSelectorPath; } - // joins selector path from `beginningPath` with every selector path in `addPaths` array - // `replacedElement` contains element that is being replaced by `addPath` - // returns array with all concatenated paths + /** + * @param {Selector[][]} beginningPath + * @param {Selector[]} addPaths + * @param {Element} replacedElement + * @param {Selector} originalSelector + * @param {Selector[][]} result + * @returns {Selector[][]} + */ function addAllReplacementsIntoPath( beginningPath, addPaths, replacedElement, originalSelector, result) { + /** @type {number} */ let j; for (j = 0; j < beginningPath.length; j++) { const newSelectorPath = addReplacementIntoPath(beginningPath[j], addPaths, replacedElement, originalSelector); @@ -665,8 +809,15 @@ class Ruleset extends Node { return result; } + /** + * @param {Element[]} elements + * @param {Selector[][]} selectors + */ function mergeElementsOnToSelectors(elements, selectors) { - let i, sel; + /** @type {number} */ + let i; + /** @type {Selector[]} */ + let sel; if (elements.length === 0) { return ; @@ -687,9 +838,12 @@ class Ruleset extends Node { } } - // replace all parent selectors inside `inSelector` by content of `context` array - // resulting selectors are returned inside `paths` array - // returns true if `inSelector` contained at least one parent selector + /** + * @param {Selector[][]} paths + * @param {Selector[][]} context + * @param {Selector} inSelector + * @returns {boolean} + */ function replaceParentSelector(paths, context, inSelector) { // The paths are [[Selector]] // The first list is a list of comma separated selectors @@ -701,14 +855,40 @@ class Ruleset extends Node { // } // == [[.a] [.c]] [[.b] [.c]] // - let i, j, k, currentElements, newSelectors, selectorsMultiplied, sel, el, hadParentSelector = false, length, lastSelector; + /** @type {number} */ + let i; + /** @type {number} */ + let j; + /** @type {number} */ + let k; + /** @type {Element[]} */ + let currentElements; + /** @type {Selector[][]} */ + let newSelectors; + /** @type {Selector[][]} */ + let selectorsMultiplied; + /** @type {Selector[]} */ + let sel; + /** @type {Element} */ + let el; + let hadParentSelector = false; + /** @type {number} */ + let length; + /** @type {Selector} */ + let lastSelector; + + /** + * @param {Element} element + * @returns {Selector | null} + */ function findNestedSelector(element) { + /** @type {Node} */ let maybeSelector; if (!(element.value instanceof Paren)) { return null; } - maybeSelector = element.value.value; + maybeSelector = /** @type {Node} */ (element.value.value); if (!(maybeSelector instanceof Selector)) { return null; } @@ -734,8 +914,11 @@ class Ruleset extends Node { // on to the current list of selectors to add mergeElementsOnToSelectors(currentElements, newSelectors); + /** @type {Selector[][]} */ const nestedPaths = []; + /** @type {boolean | undefined} */ let replaced; + /** @type {Selector[][]} */ const replacedNewSelectors = []; // Check if this is a comma-separated selector list inside the paren @@ -744,9 +927,12 @@ class Ruleset extends Node { if (hasSubSelectors) { // Process each sub-selector individually + /** @type {(Element | Selector)[]} */ + /** @type {(Element | Selector)[]} */ const resolvedElements = []; for (const subEl of nestedSelector.elements) { if (subEl instanceof Selector) { + /** @type {Selector[][]} */ const subPaths = []; const subReplaced = replaceParentSelector(subPaths, context, subEl); replaced = replaced || subReplaced; @@ -759,7 +945,7 @@ class Ruleset extends Node { resolvedElements.push(subEl); } } - hadParentSelector = hadParentSelector || replaced; + hadParentSelector = hadParentSelector || /** @type {boolean} */ (replaced); const resolvedNestedSelector = new Selector(resolvedElements); const replacementSelector = createSelector(createParenthesis([resolvedNestedSelector], el), el); addAllReplacementsIntoPath(newSelectors, [replacementSelector], el, inSelector, replacedNewSelectors); @@ -834,6 +1020,10 @@ class Ruleset extends Node { return hadParentSelector; } + /** + * @param {VisibilityInfo} visibilityInfo + * @param {Selector} deriveFrom + */ function deriveSelector(visibilityInfo, deriveFrom) { const newSelector = deriveFrom.createDerived(deriveFrom.elements, deriveFrom.extendList, deriveFrom.evaldCondition); newSelector.copyVisibilityInfo(visibilityInfo); @@ -841,7 +1031,12 @@ class Ruleset extends Node { } // joinSelector code follows - let i, newPaths, hadParentSelector; + /** @type {number} */ + let i; + /** @type {Selector[][]} */ + let newPaths; + /** @type {boolean} */ + let hadParentSelector; newPaths = []; hadParentSelector = replaceParentSelector(newPaths, context, selector); diff --git a/packages/less/lib/less/tree/selector.js b/packages/less/lib/less/tree/selector.js index b537d78b1..fb1cf39cc 100644 --- a/packages/less/lib/less/tree/selector.js +++ b/packages/less/lib/less/tree/selector.js @@ -1,28 +1,47 @@ +// @ts-check import Node from './node.js'; import Element from './element.js'; import LessError from '../less-error.js'; import * as utils from '../utils.js'; import Parser from '../parser/parser.js'; +/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo, TreeVisitor } from './node.js' */ + class Selector extends Node { get type() { return 'Selector'; } + /** + * @param {(Element | Selector)[] | string} [elements] + * @param {Node[] | null} [extendList] + * @param {Node | null} [condition] + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + * @param {VisibilityInfo} [visibilityInfo] + */ constructor(elements, extendList, condition, index, currentFileInfo, visibilityInfo) { super(); + /** @type {Node[] | null | undefined} */ this.extendList = extendList; + /** @type {Node | null | undefined} */ this.condition = condition; + /** @type {boolean | Node} */ this.evaldCondition = !condition; this._index = index; this._fileInfo = currentFileInfo; + /** @type {Element[]} */ this.elements = this.getElements(elements); + /** @type {string[] | undefined} */ this.mixinElements_ = undefined; + /** @type {boolean | undefined} */ + this.mediaEmpty = undefined; this.copyVisibilityInfo(visibilityInfo); this.setParent(this.elements, this); } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.elements) { - this.elements = visitor.visitArray(this.elements); + this.elements = /** @type {Element[]} */ (visitor.visitArray(this.elements)); } if (this.extendList) { this.extendList = visitor.visitArray(this.extendList); @@ -32,6 +51,11 @@ class Selector extends Node { } } + /** + * @param {Element[]} elements + * @param {Node[] | null} [extendList] + * @param {boolean | Node} [evaldCondition] + */ createDerived(elements, extendList, evaldCondition) { elements = this.getElements(elements); const newSelector = new Selector(elements, extendList || this.extendList, @@ -41,25 +65,30 @@ class Selector extends Node { return newSelector; } + /** + * @param {(Element | Selector)[] | string | null | undefined} els + * @returns {Element[]} + */ getElements(els) { if (!els) { return [new Element('', '&', false, this._index, this._fileInfo)]; } if (typeof els === 'string') { - new Parser(this.parse.context, this.parse.importManager, this._fileInfo, this._index).parseNode( + const parse = this.parse; + new (/** @type {new (...args: unknown[]) => { parseNode: Function }} */ (/** @type {unknown} */ (Parser)))(parse.context, parse.importManager, this._fileInfo, this._index).parseNode( els, ['selector'], - function(err, result) { + function(/** @type {{ index: number, message: string } | null} */ err, /** @type {Selector[]} */ result) { if (err) { throw new LessError({ index: err.index, message: err.message - }, this.parse.imports, this._fileInfo.filename); + }, parse.imports, /** @type {string} */ (/** @type {FileInfo} */ (this._fileInfo).filename)); } els = result[0].elements; }); } - return els; + return /** @type {Element[]} */ (els); } createEmptySelectors() { @@ -68,19 +97,24 @@ class Selector extends Node { return sels; } + /** + * @param {Selector} other + * @returns {number} + */ match(other) { const elements = this.elements; const len = elements.length; let olen; let i; - other = other.mixinElements(); - olen = other.length; + /** @type {string[]} */ + const mixinEls = other.mixinElements(); + olen = mixinEls.length; if (olen === 0 || len < olen) { return 0; } else { for (i = 0; i < olen; i++) { - if (elements[i].value !== other[i]) { + if (elements[i].value !== mixinEls[i]) { return 0; } } @@ -89,13 +123,15 @@ class Selector extends Node { return olen; // return number of matched elements } + /** @returns {string[]} */ mixinElements() { if (this.mixinElements_) { return this.mixinElements_; } + /** @type {string[] | null} */ let elements = this.elements.map( function(v) { - return v.combinator.value + (v.value.value || v.value); + return /** @type {string} */ (v.combinator.value) + (/** @type {{ value: string }} */ (v.value).value || v.value); }).join('').match(/[,&#*.\w-]([\w-]|(\\.))*/g); if (elements) { @@ -116,9 +152,11 @@ class Selector extends Node { (this.elements[0].combinator.value === ' ' || this.elements[0].combinator.value === ''); } + /** @param {EvalContext} context */ eval(context) { const evaldCondition = this.condition && this.condition.eval(context); let elements = this.elements; + /** @type {Node[] | null | undefined} */ let extendList = this.extendList; if (elements) { @@ -139,9 +177,13 @@ class Selector extends Node { return this.createDerived(elements, extendList, evaldCondition); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { let i, element; - if ((!context || !context.firstSelector) && this.elements[0].combinator.value === '') { + if ((!context || !/** @type {EvalContext & { firstSelector?: boolean }} */ (context).firstSelector) && this.elements[0].combinator.value === '') { output.add(' ', this.fileInfo(), this.getIndex()); } for (i = 0; i < this.elements.length; i++) { diff --git a/packages/less/lib/less/tree/unicode-descriptor.js b/packages/less/lib/less/tree/unicode-descriptor.js index 20bb8b52b..8b4a09d67 100644 --- a/packages/less/lib/less/tree/unicode-descriptor.js +++ b/packages/less/lib/less/tree/unicode-descriptor.js @@ -1,8 +1,10 @@ +// @ts-check import Node from './node.js'; class UnicodeDescriptor extends Node { get type() { return 'UnicodeDescriptor'; } + /** @param {string} value */ constructor(value) { super(); this.value = value; diff --git a/packages/less/lib/less/tree/unit.js b/packages/less/lib/less/tree/unit.js index 983cfd4b2..1d4619dd1 100644 --- a/packages/less/lib/less/tree/unit.js +++ b/packages/less/lib/less/tree/unit.js @@ -1,15 +1,26 @@ +// @ts-check import Node from './node.js'; import unitConversions from '../data/unit-conversions.js'; import * as utils from '../utils.js'; +/** @import { EvalContext, CSSOutput } from './node.js' */ + class Unit extends Node { get type() { return 'Unit'; } + /** + * @param {string[]} [numerator] + * @param {string[]} [denominator] + * @param {string} [backupUnit] + */ constructor(numerator, denominator, backupUnit) { super(); + /** @type {string[]} */ this.numerator = numerator ? utils.copyArray(numerator).sort() : []; + /** @type {string[]} */ this.denominator = denominator ? utils.copyArray(denominator).sort() : []; if (backupUnit) { + /** @type {string | undefined} */ this.backupUnit = backupUnit; } else if (numerator && numerator.length) { this.backupUnit = numerator[0]; @@ -20,6 +31,10 @@ class Unit extends Node { return new Unit(utils.copyArray(this.numerator), utils.copyArray(this.denominator), this.backupUnit); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { // Dimension checks the unit is singular and throws an error if in strict math mode. const strictUnits = context && context.strictUnits; @@ -40,16 +55,21 @@ class Unit extends Node { return returnStr; } + /** + * @param {Unit} other + * @returns {0 | undefined} + */ compare(other) { return this.is(other.toString()) ? 0 : undefined; } + /** @param {string} unitString */ is(unitString) { return this.toString().toUpperCase() === unitString.toUpperCase(); } isLength() { - return RegExp('^(px|em|ex|ch|rem|in|cm|mm|pc|pt|ex|vw|vh|vmin|vmax)$', 'gi').test(this.toCSS()); + return RegExp('^(px|em|ex|ch|rem|in|cm|mm|pc|pt|ex|vw|vh|vmin|vmax)$', 'gi').test(this.toCSS(/** @type {import('./node.js').EvalContext} */ ({}))); } isEmpty() { @@ -60,6 +80,7 @@ class Unit extends Node { return this.numerator.length <= 1 && this.denominator.length === 0; } + /** @param {(atomicUnit: string, denominator: boolean) => string} callback */ map(callback) { let i; @@ -72,10 +93,15 @@ class Unit extends Node { } } + /** @returns {{ [groupName: string]: string }} */ usedUnits() { + /** @type {{ [unitName: string]: number }} */ let group; + /** @type {{ [groupName: string]: string }} */ const result = {}; + /** @type {(atomicUnit: string) => string} */ let mapUnit; + /** @type {string} */ let groupName; mapUnit = function (atomicUnit) { @@ -90,7 +116,7 @@ class Unit extends Node { for (groupName in unitConversions) { // eslint-disable-next-line no-prototype-builtins if (unitConversions.hasOwnProperty(groupName)) { - group = unitConversions[groupName]; + group = /** @type {{ [unitName: string]: number }} */ (unitConversions[/** @type {keyof typeof unitConversions} */ (groupName)]); this.map(mapUnit); } @@ -100,7 +126,9 @@ class Unit extends Node { } cancel() { + /** @type {{ [unit: string]: number }} */ const counter = {}; + /** @type {string} */ let atomicUnit; let i; diff --git a/packages/less/lib/less/tree/url.js b/packages/less/lib/less/tree/url.js index f9f01642c..7ba31d05a 100644 --- a/packages/less/lib/less/tree/url.js +++ b/packages/less/lib/less/tree/url.js @@ -1,5 +1,11 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo } from './node.js' */ import Node from './node.js'; +/** + * @param {string} path + * @returns {string} + */ function escapePath(path) { return path.replace(/[()'"\s]/g, function(match) { return `\\${match}`; }); } @@ -7,26 +13,39 @@ function escapePath(path) { class URL extends Node { get type() { return 'Url'; } + /** + * @param {Node} val + * @param {number} index + * @param {FileInfo} currentFileInfo + * @param {boolean} [isEvald] + */ constructor(val, index, currentFileInfo, isEvald) { super(); this.value = val; this._index = index; this._fileInfo = currentFileInfo; + /** @type {boolean | undefined} */ this.isEvald = isEvald; } + /** @param {TreeVisitor} visitor */ accept(visitor) { - this.value = visitor.visit(this.value); + this.value = visitor.visit(/** @type {Node} */ (this.value)); } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { output.add('url('); - this.value.genCSS(context, output); + /** @type {Node} */ (this.value).genCSS(context, output); output.add(')'); } + /** @param {EvalContext} context */ eval(context) { - const val = this.value.eval(context); + const val = /** @type {Node} */ (this.value).eval(context); let rootpath; if (!this.isEvald) { @@ -34,22 +53,22 @@ class URL extends Node { rootpath = this.fileInfo() && this.fileInfo().rootpath; if (typeof rootpath === 'string' && typeof val.value === 'string' && - context.pathRequiresRewrite(val.value)) { - if (!val.quote) { + context.pathRequiresRewrite(/** @type {string} */ (val.value))) { + if (!/** @type {import('./quoted.js').default} */ (val).quote) { rootpath = escapePath(rootpath); } - val.value = context.rewritePath(val.value, rootpath); + val.value = context.rewritePath(/** @type {string} */ (val.value), rootpath); } else { - val.value = context.normalizePath(val.value); + val.value = context.normalizePath(/** @type {string} */ (val.value)); } // Add url args if enabled if (context.urlArgs) { - if (!val.value.match(/^\s*data:/)) { - const delimiter = val.value.indexOf('?') === -1 ? '?' : '&'; + if (!/** @type {string} */ (val.value).match(/^\s*data:/)) { + const delimiter = /** @type {string} */ (val.value).indexOf('?') === -1 ? '?' : '&'; const urlArgs = delimiter + context.urlArgs; - if (val.value.indexOf('#') !== -1) { - val.value = val.value.replace('#', `${urlArgs}#`); + if (/** @type {string} */ (val.value).indexOf('#') !== -1) { + val.value = /** @type {string} */ (val.value).replace('#', `${urlArgs}#`); } else { val.value += urlArgs; } diff --git a/packages/less/lib/less/tree/value.js b/packages/less/lib/less/tree/value.js index 874319032..1114705b5 100644 --- a/packages/less/lib/less/tree/value.js +++ b/packages/less/lib/less/tree/value.js @@ -1,8 +1,11 @@ +// @ts-check +/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */ import Node from './node.js'; class Value extends Node { get type() { return 'Value'; } + /** @param {Node[] | Node} value */ constructor(value) { super(); if (!value) { @@ -16,27 +19,38 @@ class Value extends Node { } } + /** @param {TreeVisitor} visitor */ accept(visitor) { if (this.value) { - this.value = visitor.visitArray(this.value); + this.value = visitor.visitArray(/** @type {Node[]} */ (this.value)); } } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { - if (this.value.length === 1) { - return this.value[0].eval(context); + const value = /** @type {Node[]} */ (this.value); + if (value.length === 1) { + return value[0].eval(context); } else { - return new Value(this.value.map(function (v) { + return new Value(value.map(function (v) { return v.eval(context); })); } } + /** + * @param {EvalContext} context + * @param {CSSOutput} output + */ genCSS(context, output) { + const value = /** @type {Node[]} */ (this.value); let i; - for (i = 0; i < this.value.length; i++) { - this.value[i].genCSS(context, output); - if (i + 1 < this.value.length) { + for (i = 0; i < value.length; i++) { + value[i].genCSS(context, output); + if (i + 1 < value.length) { output.add((context && context.compress) ? ',' : ', '); } } diff --git a/packages/less/lib/less/tree/variable-call.js b/packages/less/lib/less/tree/variable-call.js index 0a5615ed5..8c3146aef 100644 --- a/packages/less/lib/less/tree/variable-call.js +++ b/packages/less/lib/less/tree/variable-call.js @@ -1,3 +1,5 @@ +// @ts-check +/** @import { EvalContext, FileInfo } from './node.js' */ import Node from './node.js'; import Variable from './variable.js'; import Ruleset from './ruleset.js'; @@ -7,6 +9,11 @@ import LessError from '../less-error.js'; class VariableCall extends Node { get type() { return 'VariableCall'; } + /** + * @param {string} variable + * @param {number} index + * @param {FileInfo} currentFileInfo + */ constructor(variable, index, currentFileInfo) { super(); this.variable = variable; @@ -15,20 +22,26 @@ class VariableCall extends Node { this.allowRoot = true; } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { let rules; + /** @type {DetachedRuleset | Node} */ let detachedRuleset = new Variable(this.variable, this.getIndex(), this.fileInfo()).eval(context); const error = new LessError({message: `Could not evaluate variable call ${this.variable}`}); - if (!detachedRuleset.ruleset) { - if (detachedRuleset.rules) { + if (!(/** @type {DetachedRuleset} */ (detachedRuleset)).ruleset) { + const dr = /** @type {Node & { rules?: Node[] }} */ (detachedRuleset); + if (dr.rules) { rules = detachedRuleset; } else if (Array.isArray(detachedRuleset)) { - rules = new Ruleset('', detachedRuleset); + rules = new Ruleset(null, detachedRuleset); } else if (Array.isArray(detachedRuleset.value)) { - rules = new Ruleset('', detachedRuleset.value); + rules = new Ruleset(null, detachedRuleset.value); } else { throw error; @@ -36,8 +49,9 @@ class VariableCall extends Node { detachedRuleset = new DetachedRuleset(rules); } - if (detachedRuleset.ruleset) { - return detachedRuleset.callEval(context); + const dr = /** @type {DetachedRuleset} */ (detachedRuleset); + if (dr.ruleset) { + return dr.callEval(context); } throw error; } diff --git a/packages/less/lib/less/tree/variable.js b/packages/less/lib/less/tree/variable.js index 8bb9b5215..3d376d2b7 100644 --- a/packages/less/lib/less/tree/variable.js +++ b/packages/less/lib/less/tree/variable.js @@ -1,16 +1,30 @@ +// @ts-check +/** @import { EvalContext, FileInfo } from './node.js' */ import Node from './node.js'; import Call from './call.js'; +import Ruleset from './ruleset.js'; class Variable extends Node { get type() { return 'Variable'; } + /** + * @param {string} name + * @param {number} [index] + * @param {FileInfo} [currentFileInfo] + */ constructor(name, index, currentFileInfo) { super(); this.name = name; this._index = index; this._fileInfo = currentFileInfo; + /** @type {boolean | undefined} */ + this.evaluating = undefined; } + /** + * @param {EvalContext} context + * @returns {Node} + */ eval(context) { let variable, name = this.name; @@ -28,7 +42,7 @@ class Variable extends Node { this.evaluating = true; variable = this.find(context.frames, function (frame) { - const v = frame.variable(name); + const v = /** @type {Ruleset} */ (frame).variable(name); if (v) { if (v.important) { const importantScope = context.importantScope[context.importantScope.length - 1]; @@ -36,7 +50,7 @@ class Variable extends Node { } // If in calc, wrap vars in a function call to cascade evaluate args first if (context.inCalc) { - return (new Call('_SELF', [v.value])).eval(context); + return (new Call('_SELF', [v.value], 0, undefined)).eval(context); } else { return v.value.eval(context); @@ -54,6 +68,11 @@ class Variable extends Node { } } + /** + * @param {Node[]} obj + * @param {(frame: Node) => Node | undefined} fun + * @returns {Node | null} + */ find(obj, fun) { for (let i = 0, r; i < obj.length; i++) { r = fun.call(obj, obj[i]); diff --git a/packages/less/package.json b/packages/less/package.json index 3353c82d5..8c25dcd50 100644 --- a/packages/less/package.json +++ b/packages/less/package.json @@ -53,9 +53,9 @@ "grunt": "grunt", "lint": "eslint '**/*.{ts,js}'", "lint:fix": "eslint '**/*.{ts,js}' --fix", - "typecheck": "tsc", + "typecheck": "tsc --noEmit", "build": "node build/rollup.js --dist", - "prepublishOnly": "grunt dist" + "prepublishOnly": "npm run typecheck && grunt dist" }, "optionalDependencies": { "errno": "^0.1.1", From 68959fd1b278b90f1e0910b723a404d5b8d0480f Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Tue, 10 Mar 2026 09:57:52 -0700 Subject: [PATCH 2/2] fix: remove duplicate JSDoc type annotation in ruleset.js --- packages/less/lib/less/tree/ruleset.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/less/lib/less/tree/ruleset.js b/packages/less/lib/less/tree/ruleset.js index ae780ad31..1e4b7c0e2 100644 --- a/packages/less/lib/less/tree/ruleset.js +++ b/packages/less/lib/less/tree/ruleset.js @@ -928,7 +928,6 @@ class Ruleset extends Node { if (hasSubSelectors) { // Process each sub-selector individually /** @type {(Element | Selector)[]} */ - /** @type {(Element | Selector)[]} */ const resolvedElements = []; for (const subEl of nestedSelector.elements) { if (subEl instanceof Selector) {