first commit
This commit is contained in:
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* CSS Language Object
|
||||
* @implements {Language<{ LangOptions: CSSLanguageOptions; Code: CSSSourceCode; RootNode: StyleSheetPlain; Node: CssNodePlain}>}
|
||||
*/
|
||||
export class CSSLanguage implements Language {
|
||||
/**
|
||||
* The type of file to read.
|
||||
* @type {"text"}
|
||||
*/
|
||||
fileType: "text";
|
||||
/**
|
||||
* The line number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
lineStart: 0 | 1;
|
||||
/**
|
||||
* The column number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
columnStart: 0 | 1;
|
||||
/**
|
||||
* The name of the key that holds the type of the node.
|
||||
* @type {string}
|
||||
*/
|
||||
nodeTypeKey: string;
|
||||
/**
|
||||
* The visitor keys for the CSSTree AST.
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
visitorKeys: Record<string, string[]>;
|
||||
/**
|
||||
* The default language options.
|
||||
* @type {CSSLanguageOptions}
|
||||
*/
|
||||
defaultLanguageOptions: CSSLanguageOptions;
|
||||
/**
|
||||
* Validates the language options.
|
||||
* @param {CSSLanguageOptions} languageOptions The language options to validate.
|
||||
* @returns {void}
|
||||
* @throws {TypeError} When the language options are invalid.
|
||||
*/
|
||||
validateLanguageOptions(languageOptions: CSSLanguageOptions): void;
|
||||
/**
|
||||
* Normalizes the language options so they can be serialized.
|
||||
* @param {CSSLanguageOptions} languageOptions The language options to normalize.
|
||||
* @returns {CSSLanguageOptions} The normalized language options.
|
||||
*/
|
||||
normalizeLanguageOptions(languageOptions: CSSLanguageOptions): CSSLanguageOptions;
|
||||
/**
|
||||
* Parses the given file into an AST.
|
||||
* @param {File} file The virtual file to parse.
|
||||
* @param {Object} [context] The parsing context.
|
||||
* @param {CSSLanguageOptions} [context.languageOptions] The language options to use for parsing.
|
||||
* @returns {CSSParseResult} The result of parsing.
|
||||
*/
|
||||
parse(file: File, { languageOptions }?: {
|
||||
languageOptions?: CSSLanguageOptions;
|
||||
}): CSSParseResult;
|
||||
/**
|
||||
* Creates a new `CSSSourceCode` object from the given information.
|
||||
* @param {File} file The virtual file to create a `CSSSourceCode` object from.
|
||||
* @param {CSSOkParseResult} parseResult The result returned from `parse()`.
|
||||
* @returns {CSSSourceCode} The new `CSSSourceCode` object.
|
||||
*/
|
||||
createSourceCode(file: File, parseResult: CSSOkParseResult): CSSSourceCode;
|
||||
}
|
||||
export type CSSOkParseResult = OkParseResult<StyleSheetPlain> & {
|
||||
comments: Comment[];
|
||||
lexer: Lexer;
|
||||
};
|
||||
export type CSSParseResult = ParseResult<StyleSheetPlain>;
|
||||
/**
|
||||
* DefaultSyntaxConfig type representing the structure returned by `@eslint/css-tree/definition-syntax-data`.
|
||||
* This type is defined inline because it's not exported from the main `@eslint/css-tree` package.
|
||||
*/
|
||||
export type DefaultSyntaxConfig = Pick<SyntaxConfig, "atrules" | "types" | "properties">;
|
||||
export type SyntaxExtensionCallback = (defaultSyntax: DefaultSyntaxConfig) => Partial<SyntaxConfig>;
|
||||
export type CSSLanguageOptions = {
|
||||
/**
|
||||
* Whether to be tolerant of recoverable parsing errors.
|
||||
*/
|
||||
tolerant?: boolean;
|
||||
/**
|
||||
* Custom syntax to use for parsing.
|
||||
*/
|
||||
customSyntax?: Partial<SyntaxConfig> | SyntaxExtensionCallback;
|
||||
};
|
||||
import type { Language } from "@eslint/core";
|
||||
import type { File } from "@eslint/core";
|
||||
import { CSSSourceCode } from "./css-source-code.js";
|
||||
import type { StyleSheetPlain } from "@eslint/css-tree";
|
||||
import type { OkParseResult } from "@eslint/core";
|
||||
import type { Comment } from "@eslint/css-tree";
|
||||
import type { Lexer } from "@eslint/css-tree";
|
||||
import type { ParseResult } from "@eslint/core";
|
||||
import type { SyntaxConfig } from "@eslint/css-tree";
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* @fileoverview The CSSLanguage class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//------------------------------------------------------------------------------
|
||||
// Imports
|
||||
//------------------------------------------------------------------------------
|
||||
import { parse as originalParse, lexer as originalLexer, fork, toPlainObject, tokenTypes, } from "@eslint/css-tree";
|
||||
import defaultSyntax from "@eslint/css-tree/definition-syntax-data";
|
||||
import { CSSSourceCode } from "./css-source-code.js";
|
||||
import { visitorKeys } from "./css-visitor-keys.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CssNodePlain, Comment, Lexer, StyleSheetPlain, SyntaxConfig } from "@eslint/css-tree"
|
||||
* @import { Language, OkParseResult, ParseResult, File, FileError } from "@eslint/core";
|
||||
*/
|
||||
/** @typedef {OkParseResult<StyleSheetPlain> & { comments: Comment[], lexer: Lexer }} CSSOkParseResult */
|
||||
/** @typedef {ParseResult<StyleSheetPlain>} CSSParseResult */
|
||||
/**
|
||||
* DefaultSyntaxConfig type representing the structure returned by `@eslint/css-tree/definition-syntax-data`.
|
||||
* This type is defined inline because it's not exported from the main `@eslint/css-tree` package.
|
||||
* @typedef {Pick<SyntaxConfig, "atrules" | "types" | "properties">} DefaultSyntaxConfig
|
||||
*/
|
||||
/**
|
||||
* @typedef {(defaultSyntax: DefaultSyntaxConfig) => Partial<SyntaxConfig>} SyntaxExtensionCallback
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object} CSSLanguageOptions
|
||||
* @property {boolean} [tolerant] Whether to be tolerant of recoverable parsing errors.
|
||||
* @property {Partial<SyntaxConfig> | SyntaxExtensionCallback} [customSyntax] Custom syntax to use for parsing.
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const blockOpenerTokenTypes = new Map([
|
||||
[tokenTypes.Function, ")"],
|
||||
[tokenTypes.LeftCurlyBracket, "}"],
|
||||
[tokenTypes.LeftParenthesis, ")"],
|
||||
[tokenTypes.LeftSquareBracket, "]"],
|
||||
]);
|
||||
const blockCloserTokenTypes = new Map([
|
||||
[tokenTypes.RightCurlyBracket, "{"],
|
||||
[tokenTypes.RightParenthesis, "("],
|
||||
[tokenTypes.RightSquareBracket, "["],
|
||||
]);
|
||||
/**
|
||||
* Recursively replaces all function values in an object with boolean true.
|
||||
* Used to make objects serializable for JSON output.
|
||||
* @param {Record<string,any>|unknown[]|unknown} object The object to process.
|
||||
* @returns {Record<string,any>|unknown[]|unknown} A copy of the object with all functions replaced by true.
|
||||
*/
|
||||
function replaceFunctions(object) {
|
||||
if (typeof object !== "object" || object === null) {
|
||||
return object;
|
||||
}
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(replaceFunctions);
|
||||
}
|
||||
const result = {};
|
||||
for (const key of Object.keys(object)) {
|
||||
const value = object[key];
|
||||
if (typeof value === "function") {
|
||||
result[key] = true;
|
||||
}
|
||||
else if (typeof value === "object" && value !== null) {
|
||||
result[key] = replaceFunctions(value);
|
||||
}
|
||||
else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* CSS Language Object
|
||||
* @implements {Language<{ LangOptions: CSSLanguageOptions; Code: CSSSourceCode; RootNode: StyleSheetPlain; Node: CssNodePlain}>}
|
||||
*/
|
||||
export class CSSLanguage {
|
||||
/**
|
||||
* The type of file to read.
|
||||
* @type {"text"}
|
||||
*/
|
||||
fileType = "text";
|
||||
/**
|
||||
* The line number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
lineStart = 1;
|
||||
/**
|
||||
* The column number at which the parser starts counting.
|
||||
* @type {0|1}
|
||||
*/
|
||||
columnStart = 1;
|
||||
/**
|
||||
* The name of the key that holds the type of the node.
|
||||
* @type {string}
|
||||
*/
|
||||
nodeTypeKey = "type";
|
||||
/**
|
||||
* The visitor keys for the CSSTree AST.
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
visitorKeys = visitorKeys;
|
||||
/**
|
||||
* The default language options.
|
||||
* @type {CSSLanguageOptions}
|
||||
*/
|
||||
defaultLanguageOptions = {
|
||||
tolerant: false,
|
||||
};
|
||||
/**
|
||||
* Validates the language options.
|
||||
* @param {CSSLanguageOptions} languageOptions The language options to validate.
|
||||
* @returns {void}
|
||||
* @throws {TypeError} When the language options are invalid.
|
||||
*/
|
||||
validateLanguageOptions(languageOptions) {
|
||||
if ("tolerant" in languageOptions &&
|
||||
typeof languageOptions.tolerant !== "boolean") {
|
||||
throw new TypeError("Expected a boolean value for 'tolerant' option.");
|
||||
}
|
||||
if ("customSyntax" in languageOptions) {
|
||||
if (typeof languageOptions.customSyntax !== "object" &&
|
||||
typeof languageOptions.customSyntax !== "function") {
|
||||
throw new TypeError("Expected an object or function value for 'customSyntax' option.");
|
||||
}
|
||||
if (typeof languageOptions.customSyntax === "object" &&
|
||||
languageOptions.customSyntax === null) {
|
||||
throw new TypeError("Expected an object or function value for 'customSyntax' option.");
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Normalizes the language options so they can be serialized.
|
||||
* @param {CSSLanguageOptions} languageOptions The language options to normalize.
|
||||
* @returns {CSSLanguageOptions} The normalized language options.
|
||||
*/
|
||||
normalizeLanguageOptions(languageOptions) {
|
||||
// if there's no custom syntax then no changes are necessary
|
||||
if (!languageOptions?.customSyntax) {
|
||||
return languageOptions;
|
||||
}
|
||||
// Shallow copy
|
||||
const clone = { ...languageOptions };
|
||||
// If customSyntax is a function, call it with the default syntax to get the config object
|
||||
if (typeof languageOptions.customSyntax === "function") {
|
||||
clone.customSyntax = languageOptions.customSyntax(defaultSyntax);
|
||||
}
|
||||
Object.defineProperty(clone, "toJSON", {
|
||||
value() {
|
||||
// another shallow copy
|
||||
const result = { ...this };
|
||||
result.customSyntax = replaceFunctions(result.customSyntax);
|
||||
return result;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return clone;
|
||||
}
|
||||
/**
|
||||
* Parses the given file into an AST.
|
||||
* @param {File} file The virtual file to parse.
|
||||
* @param {Object} [context] The parsing context.
|
||||
* @param {CSSLanguageOptions} [context.languageOptions] The language options to use for parsing.
|
||||
* @returns {CSSParseResult} The result of parsing.
|
||||
*/
|
||||
parse(file, { languageOptions = {} } = {}) {
|
||||
// Note: BOM already removed
|
||||
const text = /** @type {string} */ (file.body);
|
||||
/** @type {Comment[]} */
|
||||
const comments = [];
|
||||
/** @type {FileError[]} */
|
||||
const errors = [];
|
||||
const { tolerant } = languageOptions;
|
||||
const { parse, lexer } = languageOptions.customSyntax
|
||||
? fork(
|
||||
/** @type {Partial<SyntaxConfig>} */ (languageOptions.customSyntax))
|
||||
: { parse: originalParse, lexer: originalLexer };
|
||||
/*
|
||||
* Check for parsing errors first. If there's a parsing error, nothing
|
||||
* else can happen. However, a parsing error does not throw an error
|
||||
* from this method - it's just considered a fatal error message, a
|
||||
* problem that ESLint identified just like any other.
|
||||
*/
|
||||
try {
|
||||
const root = toPlainObject(parse(text, {
|
||||
filename: file.path,
|
||||
positions: true,
|
||||
onComment(value, loc) {
|
||||
comments.push({
|
||||
type: "Comment",
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
},
|
||||
onParseError(error) {
|
||||
if (!tolerant) {
|
||||
errors.push(error);
|
||||
}
|
||||
},
|
||||
onToken(type, start, end, index) {
|
||||
if (tolerant) {
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
// these already generate errors
|
||||
case tokenTypes.BadString:
|
||||
case tokenTypes.BadUrl:
|
||||
break;
|
||||
default:
|
||||
/* eslint-disable new-cap -- This is a valid call */
|
||||
if (this.isBlockOpenerTokenType(type)) {
|
||||
if (this.getBlockTokenPairIndex(index) ===
|
||||
-1) {
|
||||
const loc = this.getRangeLocation(start, end);
|
||||
errors.push(parse.SyntaxError(`Missing closing ${blockOpenerTokenTypes.get(type)}`, text, start, loc.start.line, loc.start.column));
|
||||
}
|
||||
}
|
||||
else if (this.isBlockCloserTokenType(type)) {
|
||||
if (this.getBlockTokenPairIndex(index) ===
|
||||
-1) {
|
||||
const loc = this.getRangeLocation(start, end);
|
||||
errors.push(parse.SyntaxError(`Missing opening ${blockCloserTokenTypes.get(type)}`, text, start, loc.start.line, loc.start.column));
|
||||
}
|
||||
}
|
||||
/* eslint-enable new-cap -- This is a valid call */
|
||||
}
|
||||
},
|
||||
}));
|
||||
if (errors.length) {
|
||||
return {
|
||||
ok: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
ast: /** @type {StyleSheetPlain} */ (root),
|
||||
comments,
|
||||
lexer,
|
||||
};
|
||||
}
|
||||
catch (ex) {
|
||||
return {
|
||||
ok: false,
|
||||
errors: [ex],
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates a new `CSSSourceCode` object from the given information.
|
||||
* @param {File} file The virtual file to create a `CSSSourceCode` object from.
|
||||
* @param {CSSOkParseResult} parseResult The result returned from `parse()`.
|
||||
* @returns {CSSSourceCode} The new `CSSSourceCode` object.
|
||||
*/
|
||||
createSourceCode(file, parseResult) {
|
||||
return new CSSSourceCode({
|
||||
text: /** @type {string} */ (file.body),
|
||||
ast: parseResult.ast,
|
||||
comments: parseResult.comments,
|
||||
lexer: parseResult.lexer,
|
||||
});
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* CSS Source Code Object.
|
||||
* @extends {TextSourceCodeBase<{LangOptions: CSSLanguageOptions, RootNode: StyleSheetPlain, SyntaxElementWithLoc: CSSSyntaxElement, ConfigNode: Comment}>}
|
||||
*/
|
||||
export class CSSSourceCode extends TextSourceCodeBase<{
|
||||
LangOptions: CSSLanguageOptions;
|
||||
RootNode: StyleSheetPlain;
|
||||
SyntaxElementWithLoc: CSSSyntaxElement;
|
||||
ConfigNode: Comment;
|
||||
}> {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.text The source code text.
|
||||
* @param {StyleSheetPlain} options.ast The root AST node.
|
||||
* @param {Array<Comment>} options.comments The comment nodes in the source code.
|
||||
* @param {Lexer} options.lexer The lexer used to parse the source code.
|
||||
*/
|
||||
constructor({ text, ast, comments, lexer }: {
|
||||
text: string;
|
||||
ast: StyleSheetPlain;
|
||||
comments: Array<Comment>;
|
||||
lexer: Lexer;
|
||||
});
|
||||
/**
|
||||
* The comment node in the source code.
|
||||
* @type {Array<Comment>|undefined}
|
||||
*/
|
||||
comments: Array<Comment> | undefined;
|
||||
/**
|
||||
* The lexer for this instance.
|
||||
* @type {Lexer}
|
||||
*/
|
||||
lexer: Lexer;
|
||||
/**
|
||||
* Returns an array of all inline configuration nodes found in the
|
||||
* source code.
|
||||
* @returns {Array<Comment>} An array of all inline configuration nodes.
|
||||
*/
|
||||
getInlineConfigNodes(): Array<Comment>;
|
||||
/**
|
||||
* Returns directives that enable or disable rules along with any problems
|
||||
* encountered while parsing the directives.
|
||||
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
||||
* that ESLint needs to further process the directives.
|
||||
*/
|
||||
getDisableDirectives(): {
|
||||
problems: Array<FileProblem>;
|
||||
directives: Array<Directive>;
|
||||
};
|
||||
/**
|
||||
* Returns inline rule configurations along with any problems
|
||||
* encountered while parsing the configurations.
|
||||
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:CssLocationRange}>}} Information
|
||||
* that ESLint needs to further process the rule configurations.
|
||||
*/
|
||||
applyInlineConfig(): {
|
||||
problems: Array<FileProblem>;
|
||||
configs: Array<{
|
||||
config: {
|
||||
rules: RulesConfig;
|
||||
};
|
||||
loc: CssLocationRange;
|
||||
}>;
|
||||
};
|
||||
/**
|
||||
* Traverse the source code and return the steps that were taken.
|
||||
* @returns {Iterable<CSSTraversalStep>} The steps that were taken while traversing the source code.
|
||||
*/
|
||||
traverse(): Iterable<CSSTraversalStep>;
|
||||
#private;
|
||||
}
|
||||
import type { CSSLanguageOptions } from "./css-language.js";
|
||||
import type { StyleSheetPlain } from "@eslint/css-tree";
|
||||
import type { CSSSyntaxElement } from "../types.js";
|
||||
import type { Comment } from "@eslint/css-tree";
|
||||
import { TextSourceCodeBase } from "@eslint/plugin-kit";
|
||||
import type { Lexer } from "@eslint/css-tree";
|
||||
import type { FileProblem } from "@eslint/core";
|
||||
import { Directive } from "@eslint/plugin-kit";
|
||||
import type { RulesConfig } from "@eslint/core";
|
||||
import type { CssLocationRange } from "@eslint/css-tree";
|
||||
/**
|
||||
* A class to represent a step in the traversal process.
|
||||
*/
|
||||
declare class CSSTraversalStep extends VisitNodeStep {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the step.
|
||||
* @param {CssNode} options.target The target of the step.
|
||||
* @param {1|2} options.phase The phase of the step.
|
||||
* @param {Array<any>} options.args The arguments of the step.
|
||||
*/
|
||||
constructor({ target, phase, args }: {
|
||||
target: CssNode;
|
||||
phase: 1 | 2;
|
||||
args: Array<any>;
|
||||
});
|
||||
/**
|
||||
* The target of the step.
|
||||
* @type {CssNode}
|
||||
*/
|
||||
target: CssNode;
|
||||
}
|
||||
import { VisitNodeStep } from "@eslint/plugin-kit";
|
||||
import type { CssNode } from "@eslint/css-tree";
|
||||
export {};
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* @fileoverview The CSSSourceCode class.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { VisitNodeStep, TextSourceCodeBase, ConfigCommentParser, Directive, } from "@eslint/plugin-kit";
|
||||
import { visitorKeys } from "./css-visitor-keys.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CssNode, CssNodePlain, CssLocationRange, Comment, Lexer, StyleSheetPlain } from "@eslint/css-tree"
|
||||
* @import { SourceRange, FileProblem, DirectiveType, RulesConfig } from "@eslint/core"
|
||||
* @import { CSSSyntaxElement } from "../types.js"
|
||||
* @import { CSSLanguageOptions } from "./css-language.js"
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const commentParser = new ConfigCommentParser();
|
||||
const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
|
||||
/**
|
||||
* A class to represent a step in the traversal process.
|
||||
*/
|
||||
class CSSTraversalStep extends VisitNodeStep {
|
||||
/**
|
||||
* The target of the step.
|
||||
* @type {CssNode}
|
||||
*/
|
||||
target = undefined;
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the step.
|
||||
* @param {CssNode} options.target The target of the step.
|
||||
* @param {1|2} options.phase The phase of the step.
|
||||
* @param {Array<any>} options.args The arguments of the step.
|
||||
*/
|
||||
constructor({ target, phase, args }) {
|
||||
super({ target, phase, args });
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* CSS Source Code Object.
|
||||
* @extends {TextSourceCodeBase<{LangOptions: CSSLanguageOptions, RootNode: StyleSheetPlain, SyntaxElementWithLoc: CSSSyntaxElement, ConfigNode: Comment}>}
|
||||
*/
|
||||
export class CSSSourceCode extends TextSourceCodeBase {
|
||||
/**
|
||||
* Cached traversal steps.
|
||||
* @type {Array<CSSTraversalStep>|undefined}
|
||||
*/
|
||||
#steps;
|
||||
/**
|
||||
* Cache of parent nodes.
|
||||
* @type {WeakMap<CssNodePlain, CssNodePlain>}
|
||||
*/
|
||||
#parents = new WeakMap();
|
||||
/**
|
||||
* Collection of inline configuration comments.
|
||||
* @type {Array<Comment>}
|
||||
*/
|
||||
#inlineConfigComments;
|
||||
/**
|
||||
* The AST of the source code.
|
||||
* @type {StyleSheetPlain}
|
||||
*/
|
||||
ast = undefined;
|
||||
/**
|
||||
* The comment node in the source code.
|
||||
* @type {Array<Comment>|undefined}
|
||||
*/
|
||||
comments;
|
||||
/**
|
||||
* The lexer for this instance.
|
||||
* @type {Lexer}
|
||||
*/
|
||||
lexer;
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options The options for the instance.
|
||||
* @param {string} options.text The source code text.
|
||||
* @param {StyleSheetPlain} options.ast The root AST node.
|
||||
* @param {Array<Comment>} options.comments The comment nodes in the source code.
|
||||
* @param {Lexer} options.lexer The lexer used to parse the source code.
|
||||
*/
|
||||
constructor({ text, ast, comments, lexer }) {
|
||||
super({ text, ast, lineEndingPattern: /\r\n|[\r\n\f]/u });
|
||||
this.ast = ast;
|
||||
this.comments = comments;
|
||||
this.lexer = lexer;
|
||||
}
|
||||
/**
|
||||
* Returns the range of the given node.
|
||||
* @param {CssNodePlain} node The node to get the range of.
|
||||
* @returns {SourceRange} The range of the node.
|
||||
* @override
|
||||
*/
|
||||
getRange(node) {
|
||||
return [node.loc.start.offset, node.loc.end.offset];
|
||||
}
|
||||
/**
|
||||
* Returns an array of all inline configuration nodes found in the
|
||||
* source code.
|
||||
* @returns {Array<Comment>} An array of all inline configuration nodes.
|
||||
*/
|
||||
getInlineConfigNodes() {
|
||||
if (!this.#inlineConfigComments) {
|
||||
this.#inlineConfigComments = this.comments.filter(comment => INLINE_CONFIG.test(comment.value));
|
||||
}
|
||||
return this.#inlineConfigComments;
|
||||
}
|
||||
/**
|
||||
* Returns directives that enable or disable rules along with any problems
|
||||
* encountered while parsing the directives.
|
||||
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
|
||||
* that ESLint needs to further process the directives.
|
||||
*/
|
||||
getDisableDirectives() {
|
||||
/** @type {Array<FileProblem>} */
|
||||
const problems = [];
|
||||
/** @type {Array<Directive>} */
|
||||
const directives = [];
|
||||
this.getInlineConfigNodes().forEach(comment => {
|
||||
const { label, value, justification } = commentParser.parseDirective(comment.value);
|
||||
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
|
||||
if (label === "eslint-disable-line" &&
|
||||
comment.loc.start.line !== comment.loc.end.line) {
|
||||
const message = `${label} comment should not span multiple lines.`;
|
||||
problems.push({
|
||||
ruleId: null,
|
||||
message,
|
||||
loc: comment.loc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
switch (label) {
|
||||
case "eslint-disable":
|
||||
case "eslint-enable":
|
||||
case "eslint-disable-next-line":
|
||||
case "eslint-disable-line": {
|
||||
const directiveType = label.slice("eslint-".length);
|
||||
directives.push(new Directive({
|
||||
type: /** @type {DirectiveType} */ (directiveType),
|
||||
node: comment,
|
||||
value,
|
||||
justification,
|
||||
}));
|
||||
}
|
||||
// no default
|
||||
}
|
||||
});
|
||||
return { problems, directives };
|
||||
}
|
||||
/**
|
||||
* Returns inline rule configurations along with any problems
|
||||
* encountered while parsing the configurations.
|
||||
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:CssLocationRange}>}} Information
|
||||
* that ESLint needs to further process the rule configurations.
|
||||
*/
|
||||
applyInlineConfig() {
|
||||
/** @type {Array<FileProblem>} */
|
||||
const problems = [];
|
||||
/** @type {Array<{config:{rules:RulesConfig},loc:CssLocationRange}>} */
|
||||
const configs = [];
|
||||
this.getInlineConfigNodes().forEach(comment => {
|
||||
const { label, value } = commentParser.parseDirective(comment.value);
|
||||
if (label === "eslint") {
|
||||
const parseResult = commentParser.parseJSONLikeConfig(value);
|
||||
if (parseResult.ok) {
|
||||
configs.push({
|
||||
config: {
|
||||
rules: parseResult.config,
|
||||
},
|
||||
loc: comment.loc,
|
||||
});
|
||||
}
|
||||
else {
|
||||
problems.push({
|
||||
ruleId: null,
|
||||
message:
|
||||
/** @type {{ok: false, error: { message: string }}} */ (parseResult).error.message,
|
||||
loc: comment.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
configs,
|
||||
problems,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Returns the parent of the given node.
|
||||
* @param {CssNodePlain} node The node to get the parent of.
|
||||
* @returns {CssNodePlain|undefined} The parent of the node.
|
||||
*/
|
||||
getParent(node) {
|
||||
return this.#parents.get(node);
|
||||
}
|
||||
/**
|
||||
* Traverse the source code and return the steps that were taken.
|
||||
* @returns {Iterable<CSSTraversalStep>} The steps that were taken while traversing the source code.
|
||||
*/
|
||||
traverse() {
|
||||
// Because the AST doesn't mutate, we can cache the steps
|
||||
if (this.#steps) {
|
||||
return this.#steps.values();
|
||||
}
|
||||
/** @type {Array<CSSTraversalStep>} */
|
||||
const steps = (this.#steps = []);
|
||||
// Note: We can't use `walk` from `css-tree` because it uses `CssNode` instead of `CssNodePlain`
|
||||
const visit = (node, parent) => {
|
||||
// first set the parent
|
||||
this.#parents.set(node, parent);
|
||||
// then add the step
|
||||
steps.push(new CSSTraversalStep({
|
||||
target: node,
|
||||
phase: 1,
|
||||
args: [node, parent],
|
||||
}));
|
||||
// then visit the children
|
||||
for (const key of visitorKeys[node.type] || []) {
|
||||
const child = node[key];
|
||||
if (child) {
|
||||
if (Array.isArray(child)) {
|
||||
child.forEach(grandchild => {
|
||||
visit(grandchild, node);
|
||||
});
|
||||
}
|
||||
else {
|
||||
visit(child, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
// then add the exit step
|
||||
steps.push(new CSSTraversalStep({
|
||||
target: node,
|
||||
phase: 2,
|
||||
args: [node, parent],
|
||||
}));
|
||||
};
|
||||
visit(this.ast);
|
||||
return steps;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
export namespace visitorKeys {
|
||||
let AnPlusB: any[];
|
||||
let Atrule: string[];
|
||||
let AtrulePrelude: string[];
|
||||
let AttributeSelector: string[];
|
||||
let Block: string[];
|
||||
let Brackets: string[];
|
||||
let CDC: any[];
|
||||
let CDO: any[];
|
||||
let ClassSelector: any[];
|
||||
let Combinator: any[];
|
||||
let Comment: any[];
|
||||
let Condition: string[];
|
||||
let Declaration: string[];
|
||||
let DeclarationList: string[];
|
||||
let Dimension: any[];
|
||||
let Feature: string[];
|
||||
let FeatureFunction: string[];
|
||||
let FeatureRange: string[];
|
||||
let Function: string[];
|
||||
let GeneralEnclosed: string[];
|
||||
let Hash: any[];
|
||||
let IdSelector: any[];
|
||||
let Identifier: any[];
|
||||
let Layer: any[];
|
||||
let LayerList: string[];
|
||||
let MediaQuery: string[];
|
||||
let MediaQueryList: string[];
|
||||
let NestingSelector: any[];
|
||||
let Nth: string[];
|
||||
let Number: any[];
|
||||
let Operator: any[];
|
||||
let Parentheses: string[];
|
||||
let Percentage: any[];
|
||||
let PseudoClassSelector: string[];
|
||||
let PseudoElementSelector: string[];
|
||||
let Ratio: string[];
|
||||
let Raw: any[];
|
||||
let Rule: string[];
|
||||
let Scope: string[];
|
||||
let Selector: string[];
|
||||
let SelectorList: string[];
|
||||
let String: any[];
|
||||
let StyleSheet: string[];
|
||||
let SupportsDeclaration: string[];
|
||||
let TypeSelector: any[];
|
||||
let UnicodeRange: any[];
|
||||
let Url: any[];
|
||||
let Value: string[];
|
||||
let WhiteSpace: any[];
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @fileoverview Visitor keys for the CSS Tree AST.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
export const visitorKeys = {
|
||||
AnPlusB: [],
|
||||
Atrule: ["prelude", "block"],
|
||||
AtrulePrelude: ["children"],
|
||||
AttributeSelector: ["name", "value"],
|
||||
Block: ["children"],
|
||||
Brackets: ["children"],
|
||||
CDC: [],
|
||||
CDO: [],
|
||||
ClassSelector: [],
|
||||
Combinator: [],
|
||||
Comment: [],
|
||||
Condition: ["children"],
|
||||
Declaration: ["value"],
|
||||
DeclarationList: ["children"],
|
||||
Dimension: [],
|
||||
Feature: ["value"],
|
||||
FeatureFunction: ["value"],
|
||||
FeatureRange: ["left", "middle", "right"],
|
||||
Function: ["children"],
|
||||
GeneralEnclosed: ["children"],
|
||||
Hash: [],
|
||||
IdSelector: [],
|
||||
Identifier: [],
|
||||
Layer: [],
|
||||
LayerList: ["children"],
|
||||
MediaQuery: ["condition"],
|
||||
MediaQueryList: ["children"],
|
||||
NestingSelector: [],
|
||||
Nth: ["nth", "selector"],
|
||||
Number: [],
|
||||
Operator: [],
|
||||
Parentheses: ["children"],
|
||||
Percentage: [],
|
||||
PseudoClassSelector: ["children"],
|
||||
PseudoElementSelector: ["children"],
|
||||
Ratio: ["left", "right"],
|
||||
Raw: [],
|
||||
Rule: ["prelude", "block"],
|
||||
Scope: ["root", "limit"],
|
||||
Selector: ["children"],
|
||||
SelectorList: ["children"],
|
||||
String: [],
|
||||
StyleSheet: ["children"],
|
||||
SupportsDeclaration: ["declaration"],
|
||||
TypeSelector: [],
|
||||
UnicodeRange: [],
|
||||
Url: [],
|
||||
Value: ["children"],
|
||||
WhiteSpace: [],
|
||||
};
|
||||
Reference in New Issue
Block a user