first commit
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
export default rules;
|
||||
declare const rules: {
|
||||
readonly "css/font-family-fallbacks": "error";
|
||||
readonly "css/no-duplicate-imports": "error";
|
||||
readonly "css/no-duplicate-keyframe-selectors": "error";
|
||||
readonly "css/no-empty-blocks": "error";
|
||||
readonly "css/no-important": "error";
|
||||
readonly "css/no-invalid-at-rule-placement": "error";
|
||||
readonly "css/no-invalid-at-rules": "error";
|
||||
readonly "css/no-invalid-named-grid-areas": "error";
|
||||
readonly "css/no-invalid-properties": "error";
|
||||
readonly "css/no-unmatchable-selectors": "error";
|
||||
readonly "css/use-baseline": "error";
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
const rules = /** @type {const} */ ({
|
||||
"css/font-family-fallbacks": "error",
|
||||
"css/no-duplicate-imports": "error",
|
||||
"css/no-duplicate-keyframe-selectors": "error",
|
||||
"css/no-empty-blocks": "error",
|
||||
"css/no-important": "error",
|
||||
"css/no-invalid-at-rule-placement": "error",
|
||||
"css/no-invalid-at-rules": "error",
|
||||
"css/no-invalid-named-grid-areas": "error",
|
||||
"css/no-invalid-properties": "error",
|
||||
"css/no-unmatchable-selectors": "error",
|
||||
"css/use-baseline": "error"
|
||||
});
|
||||
export default rules;
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
declare const _default: {
|
||||
"font-family-fallbacks": {
|
||||
meta: typeof rule0.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-duplicate-imports": {
|
||||
meta: typeof rule1.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-duplicate-keyframe-selectors": {
|
||||
meta: typeof rule2.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-empty-blocks": {
|
||||
meta: typeof rule3.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-important": {
|
||||
meta: typeof rule4.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-invalid-at-rule-placement": {
|
||||
meta: typeof rule5.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-invalid-at-rules": {
|
||||
meta: typeof rule6.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-invalid-named-grid-areas": {
|
||||
meta: typeof rule7.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-invalid-properties": {
|
||||
meta: typeof rule8.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"no-unmatchable-selectors": {
|
||||
meta: typeof rule9.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"prefer-logical-properties": {
|
||||
meta: typeof rule10.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"relative-font-units": {
|
||||
meta: typeof rule11.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"selector-complexity": {
|
||||
meta: typeof rule12.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"use-baseline": {
|
||||
meta: typeof rule13.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
"use-layers": {
|
||||
meta: typeof rule14.meta;
|
||||
create: (context: unknown) => any;
|
||||
};
|
||||
};
|
||||
export default _default;
|
||||
import rule0 from "../rules/font-family-fallbacks.js";
|
||||
import rule1 from "../rules/no-duplicate-imports.js";
|
||||
import rule2 from "../rules/no-duplicate-keyframe-selectors.js";
|
||||
import rule3 from "../rules/no-empty-blocks.js";
|
||||
import rule4 from "../rules/no-important.js";
|
||||
import rule5 from "../rules/no-invalid-at-rule-placement.js";
|
||||
import rule6 from "../rules/no-invalid-at-rules.js";
|
||||
import rule7 from "../rules/no-invalid-named-grid-areas.js";
|
||||
import rule8 from "../rules/no-invalid-properties.js";
|
||||
import rule9 from "../rules/no-unmatchable-selectors.js";
|
||||
import rule10 from "../rules/prefer-logical-properties.js";
|
||||
import rule11 from "../rules/relative-font-units.js";
|
||||
import rule12 from "../rules/selector-complexity.js";
|
||||
import rule13 from "../rules/use-baseline.js";
|
||||
import rule14 from "../rules/use-layers.js";
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import rule0 from "../rules/font-family-fallbacks.js";
|
||||
import rule1 from "../rules/no-duplicate-imports.js";
|
||||
import rule2 from "../rules/no-duplicate-keyframe-selectors.js";
|
||||
import rule3 from "../rules/no-empty-blocks.js";
|
||||
import rule4 from "../rules/no-important.js";
|
||||
import rule5 from "../rules/no-invalid-at-rule-placement.js";
|
||||
import rule6 from "../rules/no-invalid-at-rules.js";
|
||||
import rule7 from "../rules/no-invalid-named-grid-areas.js";
|
||||
import rule8 from "../rules/no-invalid-properties.js";
|
||||
import rule9 from "../rules/no-unmatchable-selectors.js";
|
||||
import rule10 from "../rules/prefer-logical-properties.js";
|
||||
import rule11 from "../rules/relative-font-units.js";
|
||||
import rule12 from "../rules/selector-complexity.js";
|
||||
import rule13 from "../rules/use-baseline.js";
|
||||
import rule14 from "../rules/use-layers.js";
|
||||
export default {
|
||||
"font-family-fallbacks": /** @type {{meta: typeof rule0.meta; create: (context: unknown) => any}} */ (rule0),
|
||||
"no-duplicate-imports": /** @type {{meta: typeof rule1.meta; create: (context: unknown) => any}} */ (rule1),
|
||||
"no-duplicate-keyframe-selectors": /** @type {{meta: typeof rule2.meta; create: (context: unknown) => any}} */ (rule2),
|
||||
"no-empty-blocks": /** @type {{meta: typeof rule3.meta; create: (context: unknown) => any}} */ (rule3),
|
||||
"no-important": /** @type {{meta: typeof rule4.meta; create: (context: unknown) => any}} */ (rule4),
|
||||
"no-invalid-at-rule-placement": /** @type {{meta: typeof rule5.meta; create: (context: unknown) => any}} */ (rule5),
|
||||
"no-invalid-at-rules": /** @type {{meta: typeof rule6.meta; create: (context: unknown) => any}} */ (rule6),
|
||||
"no-invalid-named-grid-areas": /** @type {{meta: typeof rule7.meta; create: (context: unknown) => any}} */ (rule7),
|
||||
"no-invalid-properties": /** @type {{meta: typeof rule8.meta; create: (context: unknown) => any}} */ (rule8),
|
||||
"no-unmatchable-selectors": /** @type {{meta: typeof rule9.meta; create: (context: unknown) => any}} */ (rule9),
|
||||
"prefer-logical-properties": /** @type {{meta: typeof rule10.meta; create: (context: unknown) => any}} */ (rule10),
|
||||
"relative-font-units": /** @type {{meta: typeof rule11.meta; create: (context: unknown) => any}} */ (rule11),
|
||||
"selector-complexity": /** @type {{meta: typeof rule12.meta; create: (context: unknown) => any}} */ (rule12),
|
||||
"use-baseline": /** @type {{meta: typeof rule13.meta; create: (context: unknown) => any}} */ (rule13),
|
||||
"use-layers": /** @type {{meta: typeof rule14.meta; create: (context: unknown) => any}} */ (rule14),
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @fileoverview CSS features extracted from the web-features package.
|
||||
* @author tools/generate-baseline.js
|
||||
*
|
||||
* THIS FILE IS AUTOGENERATED. DO NOT MODIFY DIRECTLY.
|
||||
*/
|
||||
export const BASELINE_HIGH: 10;
|
||||
export const BASELINE_LOW: 5;
|
||||
export const properties: Map<string, string>;
|
||||
export const atRules: Map<string, string>;
|
||||
export const mediaConditions: Map<string, string>;
|
||||
export const functions: Map<string, string>;
|
||||
export const units: Map<string, string>;
|
||||
export const selectors: Map<string, string>;
|
||||
export const propertyValues: Map<string, Map<string, string>>;
|
||||
+4248
File diff suppressed because it is too large
Load Diff
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @fileoverview Color information for CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
export const namedColors: Set<string>;
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @fileoverview Color information for CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
export const namedColors = new Set([
|
||||
"aliceblue",
|
||||
"antiquewhite",
|
||||
"aqua",
|
||||
"aquamarine",
|
||||
"azure",
|
||||
"beige",
|
||||
"bisque",
|
||||
"black",
|
||||
"blanchedalmond",
|
||||
"blue",
|
||||
"blueviolet",
|
||||
"brown",
|
||||
"burlywood",
|
||||
"cadetblue",
|
||||
"chartreuse",
|
||||
"chocolate",
|
||||
"coral",
|
||||
"cornflowerblue",
|
||||
"cornsilk",
|
||||
"crimson",
|
||||
"cyan",
|
||||
"darkblue",
|
||||
"darkcyan",
|
||||
"darkgoldenrod",
|
||||
"darkgray",
|
||||
"darkgreen",
|
||||
"darkgrey",
|
||||
"darkkhaki",
|
||||
"darkmagenta",
|
||||
"darkolivegreen",
|
||||
"darkorange",
|
||||
"darkorchid",
|
||||
"darkred",
|
||||
"darksalmon",
|
||||
"darkseagreen",
|
||||
"darkslateblue",
|
||||
"darkslategray",
|
||||
"darkslategrey",
|
||||
"darkturquoise",
|
||||
"darkviolet",
|
||||
"deeppink",
|
||||
"deepskyblue",
|
||||
"dimgray",
|
||||
"dimgrey",
|
||||
"dodgerblue",
|
||||
"firebrick",
|
||||
"floralwhite",
|
||||
"forestgreen",
|
||||
"fuchsia",
|
||||
"gainsboro",
|
||||
"ghostwhite",
|
||||
"gold",
|
||||
"goldenrod",
|
||||
"gray",
|
||||
"green",
|
||||
"greenyellow",
|
||||
"grey",
|
||||
"honeydew",
|
||||
"hotpink",
|
||||
"indianred",
|
||||
"indigo",
|
||||
"ivory",
|
||||
"khaki",
|
||||
"lavender",
|
||||
"lavenderblush",
|
||||
"lawngreen",
|
||||
"lemonchiffon",
|
||||
"lightblue",
|
||||
"lightcoral",
|
||||
"lightcyan",
|
||||
"lightgoldenrodyellow",
|
||||
"lightgray",
|
||||
"lightgreen",
|
||||
"lightgrey",
|
||||
"lightpink",
|
||||
"lightsalmon",
|
||||
"lightseagreen",
|
||||
"lightskyblue",
|
||||
"lightslategray",
|
||||
"lightslategrey",
|
||||
"lightsteelblue",
|
||||
"lightyellow",
|
||||
"lime",
|
||||
"limegreen",
|
||||
"linen",
|
||||
"magenta",
|
||||
"maroon",
|
||||
"mediumaquamarine",
|
||||
"mediumblue",
|
||||
"mediumorchid",
|
||||
"mediumpurple",
|
||||
"mediumseagreen",
|
||||
"mediumslateblue",
|
||||
"mediumspringgreen",
|
||||
"mediumturquoise",
|
||||
"mediumvioletred",
|
||||
"midnightblue",
|
||||
"mintcream",
|
||||
"mistyrose",
|
||||
"moccasin",
|
||||
"navajowhite",
|
||||
"navy",
|
||||
"oldlace",
|
||||
"olive",
|
||||
"olivedrab",
|
||||
"orange",
|
||||
"orangered",
|
||||
"orchid",
|
||||
"palegoldenrod",
|
||||
"palegreen",
|
||||
"paleturquoise",
|
||||
"palevioletred",
|
||||
"papayawhip",
|
||||
"peachpuff",
|
||||
"peru",
|
||||
"pink",
|
||||
"plum",
|
||||
"powderblue",
|
||||
"purple",
|
||||
"rebeccapurple",
|
||||
"red",
|
||||
"rosybrown",
|
||||
"royalblue",
|
||||
"saddlebrown",
|
||||
"salmon",
|
||||
"sandybrown",
|
||||
"seagreen",
|
||||
"seashell",
|
||||
"sienna",
|
||||
"silver",
|
||||
"skyblue",
|
||||
"slateblue",
|
||||
"slategray",
|
||||
"slategrey",
|
||||
"snow",
|
||||
"springgreen",
|
||||
"steelblue",
|
||||
"tan",
|
||||
"teal",
|
||||
"thistle",
|
||||
"tomato",
|
||||
"turquoise",
|
||||
"violet",
|
||||
"wheat",
|
||||
"white",
|
||||
"whitesmoke",
|
||||
"yellow",
|
||||
"yellowgreen",
|
||||
]);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
export default plugin;
|
||||
export { CSSSourceCode };
|
||||
export * from "./languages/css-language.js";
|
||||
export * from "./types.js";
|
||||
declare namespace plugin {
|
||||
export namespace meta {
|
||||
let name: string;
|
||||
let version: string;
|
||||
}
|
||||
export namespace languages {
|
||||
let css: CSSLanguage;
|
||||
}
|
||||
export { rules };
|
||||
export namespace configs {
|
||||
namespace recommended {
|
||||
let name_1: string;
|
||||
export { name_1 as name };
|
||||
export let plugins: {};
|
||||
export { recommendedRules as rules };
|
||||
}
|
||||
}
|
||||
}
|
||||
import { CSSSourceCode } from "./languages/css-source-code.js";
|
||||
import { CSSLanguage } from "./languages/css-language.js";
|
||||
import rules from "./build/rules.js";
|
||||
import recommendedRules from "./build/recommended-config.js";
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @fileoverview CSS plugin.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { CSSLanguage } from "./languages/css-language.js";
|
||||
import { CSSSourceCode } from "./languages/css-source-code.js";
|
||||
import recommendedRules from "./build/recommended-config.js";
|
||||
import rules from "./build/rules.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Plugin
|
||||
//-----------------------------------------------------------------------------
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: "@eslint/css",
|
||||
version: "1.4.0", // x-release-please-version
|
||||
},
|
||||
languages: {
|
||||
css: new CSSLanguage(),
|
||||
},
|
||||
rules,
|
||||
configs: {
|
||||
recommended: {
|
||||
name: "@eslint/css/recommended",
|
||||
plugins: {},
|
||||
rules: recommendedRules,
|
||||
},
|
||||
},
|
||||
};
|
||||
Object.assign(plugin.configs.recommended.plugins, { css: plugin });
|
||||
export default plugin;
|
||||
export { CSSSourceCode };
|
||||
export * from "./languages/css-language.js";
|
||||
export * from "./types.js";
|
||||
+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: [],
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "suggestion";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let useFallbackFonts: string;
|
||||
let useGenericFont: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: FontFamilyFallbacksMessageIds;
|
||||
}>): {
|
||||
"Rule > Block > Declaration"(node: any): void;
|
||||
"Rule > Block > Declaration[property='font-family'] > Value"(node: any): void;
|
||||
"Rule > Block > Declaration[property='font'] > Value"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type FontFamilyFallbacksMessageIds = "useFallbackFonts" | "useGenericFont";
|
||||
export type FontFamilyFallbacksRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: FontFamilyFallbacksMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce the use of fallback fonts and a generic font last.
|
||||
* @author Tanuj Kanti
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"useFallbackFonts" | "useGenericFont"} FontFamilyFallbacksMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: FontFamilyFallbacksMessageIds }>} FontFamilyFallbacksRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const genericFonts = new Set([
|
||||
"serif",
|
||||
"sans-serif",
|
||||
"monospace",
|
||||
"cursive",
|
||||
"fantasy",
|
||||
"system-ui",
|
||||
"ui-serif",
|
||||
"ui-sans-serif",
|
||||
"ui-monospace",
|
||||
"ui-rounded",
|
||||
"emoji",
|
||||
"math",
|
||||
"fangsong",
|
||||
]);
|
||||
/**
|
||||
* Check if the value is a CSS-wide keyword.
|
||||
* @param {string} value The value to check.
|
||||
* @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
|
||||
* @returns {boolean} True if the value is a CSS-wide keyword, false otherwise.
|
||||
*/
|
||||
function isCSSWideKeyword(value, cssWideKeywords) {
|
||||
return cssWideKeywords.has(value.trim().toLowerCase());
|
||||
}
|
||||
/**
|
||||
* Check if the node is an identifier with a CSS-wide keyword.
|
||||
* @param {Object} node The node to check.
|
||||
* @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
|
||||
* @returns {boolean} True if the node is a CSS-wide keyword identifier, false otherwise.
|
||||
*/
|
||||
function isCSSWideKeywordIdentifier(node, cssWideKeywords) {
|
||||
return (node.type === "Identifier" &&
|
||||
isCSSWideKeyword(node.name, cssWideKeywords));
|
||||
}
|
||||
/**
|
||||
* Check if the node is a CSS variable function.
|
||||
* @param {Object} node The node to check.
|
||||
* @returns {boolean} True if the node is a variable function, false otherwise.
|
||||
*/
|
||||
function isVarFunction(node) {
|
||||
return node.type === "Function" && node.name === "var";
|
||||
}
|
||||
/**
|
||||
* Report an error if the font property values do not have fallbacks or a generic font.
|
||||
* @param {string} fontPropertyValues The font property values to check.
|
||||
* @param {Object} context The ESLint context object.
|
||||
* @param {Object} node The CSS node being checked.
|
||||
* @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords) {
|
||||
if (isCSSWideKeyword(fontPropertyValues, cssWideKeywords)) {
|
||||
return;
|
||||
}
|
||||
const valueList = fontPropertyValues.split(",").map(v => v.trim());
|
||||
if (valueList.length === 1) {
|
||||
const containsGenericFont = Array.from(genericFonts).some(font => valueList[0].includes(font));
|
||||
if (!containsGenericFont) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!genericFonts.has(valueList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {FontFamilyFallbacksRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description: "Enforce use of fallback fonts and a generic font last",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/font-family-fallbacks.md",
|
||||
},
|
||||
messages: {
|
||||
useFallbackFonts: "Use fallback fonts and a generic font last.",
|
||||
useGenericFont: "Use a generic font last.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const cssWideKeywords = new Set(sourceCode.lexer.cssWideKeywords.map(keyword => keyword.toLowerCase()));
|
||||
const variableMap = new Map();
|
||||
return {
|
||||
"Rule > Block > Declaration"(node) {
|
||||
if (node.property.startsWith("--")) {
|
||||
const variableName = node.property;
|
||||
const variableValue = node.value.type === "Raw" && node.value.value;
|
||||
variableMap.set(variableName, variableValue);
|
||||
}
|
||||
},
|
||||
"Rule > Block > Declaration[property='font-family'] > Value"(node) {
|
||||
const valueArr = node.children;
|
||||
if (valueArr.length === 1) {
|
||||
if (isCSSWideKeywordIdentifier(valueArr[0], cssWideKeywords)) {
|
||||
return;
|
||||
}
|
||||
if (valueArr[0].type === "Function" &&
|
||||
valueArr[0].name === "var") {
|
||||
const variableName = valueArr[0].children[0].type === "Identifier" &&
|
||||
valueArr[0].children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
if (isCSSWideKeyword(variableValue, cssWideKeywords)) {
|
||||
return;
|
||||
}
|
||||
const variableList = variableValue
|
||||
.split(",")
|
||||
.map(v => v.trim());
|
||||
if (variableList.length === 1 &&
|
||||
!genericFonts.has(variableList[0])) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
else if (!genericFonts.has(variableList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (valueArr[0].type === "Identifier" &&
|
||||
genericFonts.has(valueArr[0].name)) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const isUsingVariable = valueArr.some(child => isVarFunction(child));
|
||||
if (isUsingVariable) {
|
||||
const fontsList = [];
|
||||
const lastNode = valueArr.at(-1);
|
||||
if (lastNode.type === "Function" &&
|
||||
lastNode.name === "var") {
|
||||
const variableName = lastNode.children[0].type === "Identifier" &&
|
||||
lastNode.children[0].name;
|
||||
const lastVariable = variableMap.get(variableName);
|
||||
if (!lastVariable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
valueArr.forEach(child => {
|
||||
if (child.type === "String") {
|
||||
fontsList.push(child.value);
|
||||
}
|
||||
if (child.type === "Identifier") {
|
||||
fontsList.push(child.name);
|
||||
}
|
||||
if (child.type === "Function" &&
|
||||
child.name === "var") {
|
||||
const variableName = child.children[0].type === "Identifier" &&
|
||||
child.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (variableValue) {
|
||||
const variableList = variableValue
|
||||
.split(",")
|
||||
.map(v => v.trim());
|
||||
fontsList.push(...variableList);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fontsList.length > 0 &&
|
||||
!genericFonts.has(fontsList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const lastFont = valueArr.at(-1);
|
||||
if (!(lastFont.type === "Identifier" &&
|
||||
genericFonts.has(lastFont.name))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rule > Block > Declaration[property='font'] > Value"(node) {
|
||||
const valueArr = node.children;
|
||||
if (valueArr.length === 1) {
|
||||
const firstValue = valueArr[0];
|
||||
// If it font is set to system font, we don't need to check for fallbacks
|
||||
if (firstValue.type === "Identifier") {
|
||||
return;
|
||||
}
|
||||
// If the value is a variable function, we need to check the variable value
|
||||
if (firstValue.type === "Function" &&
|
||||
firstValue.name === "var") {
|
||||
// Check if the function is a variable
|
||||
const variableName = firstValue.children[0].type === "Identifier" &&
|
||||
firstValue.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const isUsingVariable = valueArr.some(child => isVarFunction(child));
|
||||
if (isUsingVariable) {
|
||||
const beforOperator = [];
|
||||
const afterOperator = [];
|
||||
const operator = valueArr.find(child => child.type === "Operator" &&
|
||||
child.value === ",");
|
||||
const operatorOffset = operator && operator.loc.end.offset;
|
||||
if (operatorOffset) {
|
||||
valueArr.forEach(child => {
|
||||
if (child.loc.end.offset < operatorOffset) {
|
||||
beforOperator.push(sourceCode.getText(child).trim());
|
||||
}
|
||||
else if (child.loc.end.offset > operatorOffset) {
|
||||
afterOperator.push(sourceCode.getText(child).trim());
|
||||
}
|
||||
});
|
||||
if (afterOperator.length !== 0) {
|
||||
const usingVar = afterOperator.some(value => value.startsWith("var"));
|
||||
if (!usingVar) {
|
||||
if (!genericFonts.has(afterOperator.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (afterOperator.at(-1).startsWith("var")) {
|
||||
const lastNode = valueArr.at(-1);
|
||||
const isFunctionVar = lastNode.type === "Function" &&
|
||||
lastNode.name === "var";
|
||||
const variableName = isFunctionVar &&
|
||||
lastNode.children[0].type ===
|
||||
"Identifier" &&
|
||||
lastNode.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
const variableList = variableValue
|
||||
.split(",")
|
||||
.map(v => v.trim());
|
||||
if (variableList.length > 0 &&
|
||||
!genericFonts.has(variableList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!genericFonts.has(afterOperator.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (sourceCode
|
||||
.getText(valueArr.at(-1))
|
||||
.trim()
|
||||
.startsWith("var")) {
|
||||
const lastNode = valueArr.at(-1);
|
||||
const isFunctionVar = lastNode.type === "Function" &&
|
||||
lastNode.name === "var";
|
||||
const variableName = isFunctionVar &&
|
||||
lastNode.children[0].type ===
|
||||
"Identifier" &&
|
||||
lastNode.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
|
||||
}
|
||||
else {
|
||||
if (!genericFonts.has(sourceCode
|
||||
.getText(valueArr.at(-1))
|
||||
.trim())) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const fontPropertyValues = sourceCode.getText(node);
|
||||
if (fontPropertyValues) {
|
||||
reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let fixable: "code";
|
||||
let hasSuggestions: true;
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let duplicateImport: string;
|
||||
let removeDuplicateImportWithModifiers: string;
|
||||
let removeDuplicateImportWithoutModifiers: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoDuplicateKeysMessageIds;
|
||||
}>): {
|
||||
"Atrule[name=/^import$/i]"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoDuplicateKeysMessageIds = "duplicateImport" | "removeDuplicateImportWithModifiers" | "removeDuplicateImportWithoutModifiers";
|
||||
export type NoDuplicateImportsRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoDuplicateKeysMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent duplicate imports in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"duplicateImport" | "removeDuplicateImportWithModifiers" | "removeDuplicateImportWithoutModifiers"} NoDuplicateKeysMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateImportsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Get the end index of import statement including a following newline if present.
|
||||
* @param {string} text The full text of the source code.
|
||||
* @param {number} end The end index of the import statement.
|
||||
* @returns {number} The end index of the import statement including a following newline.
|
||||
*/
|
||||
function getImportEnd(text, end) {
|
||||
let removeEnd = end;
|
||||
// Remove the node, and also remove a following newline if present
|
||||
if (text[removeEnd] === "\r") {
|
||||
removeEnd += text[removeEnd + 1] === "\n" ? 2 : 1;
|
||||
}
|
||||
else if (text[removeEnd] === "\n" || text[removeEnd] === "\f") {
|
||||
removeEnd += 1;
|
||||
}
|
||||
return removeEnd;
|
||||
}
|
||||
/**
|
||||
* Get the modifiers of an import statement.
|
||||
* @param {Object} importNode The import node to get modifiers from.
|
||||
* @param {Object} sourceCode The source code object.
|
||||
* @returns {string[]} An array of modifiers for the import statement.
|
||||
*/
|
||||
function getImportModifiers(importNode, sourceCode) {
|
||||
const importModifiers = [];
|
||||
const importHasModifiers = importNode.prelude?.children.length > 1;
|
||||
if (importHasModifiers) {
|
||||
importNode.prelude?.children.slice(1).forEach(modifier => {
|
||||
const modifierText = sourceCode.getText(modifier).trim();
|
||||
importModifiers.push(modifierText);
|
||||
});
|
||||
}
|
||||
return importModifiers;
|
||||
}
|
||||
/**
|
||||
* Get the fix for a duplicate import statement.
|
||||
* @param {Object} fixer The fixer object.
|
||||
* @param {string} text The full text of the source code.
|
||||
* @param {number} start The start index of the import statement to fix.
|
||||
* @param {number} end The end index of the import statement to fix.
|
||||
* @param {boolean} hasModifiers A boolean indicating whether the import statement has modifiers that differ from the original import.
|
||||
* @returns {Object|null} A fix object if a fix is applicable, or null if no fix should be applied.
|
||||
*/
|
||||
function getFixForImport(fixer, text, start, end, hasModifiers) {
|
||||
const removeEnd = getImportEnd(text, end);
|
||||
if (hasModifiers) {
|
||||
return fixer.removeRange([start, removeEnd]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoDuplicateImportsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
fixable: "code",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: "Disallow duplicate @import rules",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-imports.md",
|
||||
},
|
||||
messages: {
|
||||
duplicateImport: "Unexpected duplicate @import rule for '{{url}}'.",
|
||||
removeDuplicateImportWithModifiers: "Remove duplicate @import rule with modifier(s) - {{modifiers}}.",
|
||||
removeDuplicateImportWithoutModifiers: "Remove duplicate @import rule without modifiers.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const imports = [];
|
||||
return {
|
||||
"Atrule[name=/^import$/i]"(node) {
|
||||
const url = node.prelude?.children[0].value;
|
||||
const hasImport = imports.some(importNode => importNode.prelude?.children[0].value === url);
|
||||
if (hasImport) {
|
||||
const firstImportNode = imports.find(importNode => importNode.prelude?.children[0].value === url);
|
||||
const [firstImportStart, firstImportEnd] = sourceCode.getRange(firstImportNode);
|
||||
const firstImportHasModifiers = firstImportNode.prelude?.children.length > 1;
|
||||
const nodeHasModifiers = node.prelude?.children.length > 1;
|
||||
const [start, end] = sourceCode.getRange(node);
|
||||
const text = sourceCode.text;
|
||||
const firstImportModifiers = getImportModifiers(firstImportNode, sourceCode);
|
||||
const duplicateImportModifiers = getImportModifiers(node, sourceCode);
|
||||
const hasSameModifiers = firstImportModifiers.length ===
|
||||
duplicateImportModifiers.length &&
|
||||
firstImportModifiers.every((modifier, index) => modifier === duplicateImportModifiers[index]);
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "duplicateImport",
|
||||
data: { url },
|
||||
fix(fixer) {
|
||||
const hasModifiers = (!firstImportHasModifiers &&
|
||||
!nodeHasModifiers) ||
|
||||
hasSameModifiers;
|
||||
return getFixForImport(fixer, text, start, end, hasModifiers);
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: firstImportHasModifiers
|
||||
? "removeDuplicateImportWithModifiers"
|
||||
: "removeDuplicateImportWithoutModifiers",
|
||||
data: {
|
||||
modifiers: firstImportModifiers.join(" "),
|
||||
},
|
||||
fix(fixer) {
|
||||
const hasModifiers = (firstImportHasModifiers ||
|
||||
nodeHasModifiers) &&
|
||||
!hasSameModifiers;
|
||||
return getFixForImport(fixer, text, firstImportStart, firstImportEnd, hasModifiers);
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: nodeHasModifiers
|
||||
? "removeDuplicateImportWithModifiers"
|
||||
: "removeDuplicateImportWithoutModifiers",
|
||||
data: {
|
||||
modifiers: duplicateImportModifiers.join(" "),
|
||||
},
|
||||
fix(fixer) {
|
||||
const hasModifiers = (firstImportHasModifiers ||
|
||||
nodeHasModifiers) &&
|
||||
!hasSameModifiers;
|
||||
return getFixForImport(fixer, text, start, end, hasModifiers);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
else {
|
||||
imports.push(node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let duplicateKeyframeSelector: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "duplicateKeyframeSelector";
|
||||
}>): {
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]"(): void;
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]:exit"(): void;
|
||||
Rule(node: import("@eslint/css-tree").RulePlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type DuplicateKeyframeSelectorMessageIds = "duplicateKeyframeSelector";
|
||||
export type DuplicateKeyframeSelectorRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: DuplicateKeyframeSelectorMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow duplicate selectors within keyframe blocks.
|
||||
* @author Nitin Kumar
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"duplicateKeyframeSelector"} DuplicateKeyframeSelectorMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: DuplicateKeyframeSelectorMessageIds }>} DuplicateKeyframeSelectorRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {DuplicateKeyframeSelectorRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow duplicate selectors within keyframe blocks",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-keyframe-selectors.md",
|
||||
},
|
||||
messages: {
|
||||
duplicateKeyframeSelector: "Unexpected duplicate selector '{{selector}}' found within keyframe block.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let insideKeyframes = false;
|
||||
const seen = new Map();
|
||||
return {
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]"() {
|
||||
insideKeyframes = true;
|
||||
seen.clear();
|
||||
},
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]:exit"() {
|
||||
insideKeyframes = false;
|
||||
},
|
||||
Rule(node) {
|
||||
if (!insideKeyframes) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore - children is a valid property for prelude
|
||||
const selector = node.prelude.children[0].children[0];
|
||||
let value;
|
||||
if (selector.type === "Percentage") {
|
||||
value = `${selector.value}%`;
|
||||
}
|
||||
else if (selector.type === "TypeSelector") {
|
||||
value = selector.name.toLowerCase();
|
||||
}
|
||||
else {
|
||||
value = selector.value;
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
context.report({
|
||||
loc: selector.loc,
|
||||
messageId: "duplicateKeyframeSelector",
|
||||
data: {
|
||||
selector: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
else {
|
||||
seen.set(value, true);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyBlock: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "emptyBlock";
|
||||
}>): {
|
||||
Block(node: import("@eslint/css-tree").BlockPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoEmptyBlocksMessageIds = "emptyBlock";
|
||||
export type NoEmptyBlocksRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoEmptyBlocksMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent empty blocks in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"emptyBlock"} NoEmptyBlocksMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoEmptyBlocksMessageIds }>} NoEmptyBlocksRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoEmptyBlocksRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow empty blocks",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-empty-blocks.md",
|
||||
},
|
||||
messages: {
|
||||
emptyBlock: "Unexpected empty block found.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Block(node) {
|
||||
if (node.children.length === 0) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "emptyBlock",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let hasSuggestions: true;
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unexpectedImportant: string;
|
||||
let removeImportant: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoImportantMessageIds;
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoImportantMessageIds = "unexpectedImportant" | "removeImportant";
|
||||
export type NoImportantRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoImportantMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow `!important` flags.
|
||||
* @author thecalamiity
|
||||
* @author Yann Bertrand
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"unexpectedImportant" | "removeImportant"} NoImportantMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoImportantMessageIds }>} NoImportantRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const importantPattern = /!\s*important/iu;
|
||||
const commentPattern = /\/\*[\s\S]*?\*\//gu;
|
||||
const trailingWhitespacePattern = /\s*$/u;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoImportantRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: "Disallow !important flags",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-important.md",
|
||||
},
|
||||
messages: {
|
||||
unexpectedImportant: "Unexpected !important flag found.",
|
||||
removeImportant: "Remove !important flag.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
Declaration(node) {
|
||||
if (node.important) {
|
||||
const declarationText = sourceCode.getText(node);
|
||||
const textWithoutComments = declarationText.replace(commentPattern,
|
||||
/* eslint-disable-next-line require-unicode-regexp -- we want to replace each code unit with a space */
|
||||
match => match.replace(/[^\r\n\f]/g, " "));
|
||||
const importantMatch = importantPattern.exec(textWithoutComments);
|
||||
const importantStartOffset = importantMatch.index;
|
||||
const importantEndOffset = importantStartOffset + importantMatch[0].length;
|
||||
const nodeStartOffset = node.loc.start.offset;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(nodeStartOffset + importantStartOffset),
|
||||
end: sourceCode.getLocFromIndex(nodeStartOffset + importantEndOffset),
|
||||
},
|
||||
messageId: "unexpectedImportant",
|
||||
suggest: [
|
||||
{
|
||||
messageId: "removeImportant",
|
||||
fix(fixer) {
|
||||
// Find any trailing whitespace before the `!important`
|
||||
const whitespaceEndOffset = declarationText
|
||||
.slice(0, importantStartOffset)
|
||||
.search(trailingWhitespacePattern);
|
||||
return fixer.removeRange([
|
||||
nodeStartOffset + whitespaceEndOffset,
|
||||
nodeStartOffset + importantEndOffset,
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let invalidCharsetPlacement: string;
|
||||
let invalidImportPlacement: string;
|
||||
let invalidNamespacePlacement: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidAtRulePlacementMessageIds;
|
||||
}>): {
|
||||
Atrule(node: import("@eslint/css-tree").AtrulePlain): void;
|
||||
Rule(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidAtRulePlacementMessageIds = "invalidCharsetPlacement" | "invalidImportPlacement" | "invalidNamespacePlacement";
|
||||
export type NoInvalidAtRulePlacementRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoInvalidAtRulePlacementMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce correct placement of at-rules.
|
||||
* @author thecalamiity
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"invalidCharsetPlacement" | "invalidImportPlacement" | "invalidNamespacePlacement"} NoInvalidAtRulePlacementMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulePlacementMessageIds }>} NoInvalidAtRulePlacementRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidAtRulePlacementRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow invalid placement of at-rules",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-at-rule-placement.md",
|
||||
},
|
||||
messages: {
|
||||
invalidCharsetPlacement: "@charset must be placed at the very beginning of the stylesheet, before any rules, comments, or whitespace.",
|
||||
invalidImportPlacement: "@import must be placed before all other rules, except @charset and @layer statements.",
|
||||
invalidNamespacePlacement: "@namespace must be placed before all other rules, except @charset and @import.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let hasSeenNonImportRule = false;
|
||||
let hasSeenLayerBlock = false;
|
||||
let hasSeenLayer = false;
|
||||
let hasSeenNamespace = false;
|
||||
return {
|
||||
Atrule(node) {
|
||||
const name = node.name.toLowerCase();
|
||||
if (name === "charset") {
|
||||
if (node.loc.start.line !== 1 ||
|
||||
node.loc.start.column !== 1) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidCharsetPlacement",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (name === "layer") {
|
||||
if (node.block) {
|
||||
hasSeenLayerBlock = true;
|
||||
}
|
||||
hasSeenLayer = true;
|
||||
return;
|
||||
}
|
||||
if (name === "namespace") {
|
||||
if (hasSeenNonImportRule || hasSeenLayer) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidNamespacePlacement",
|
||||
});
|
||||
}
|
||||
hasSeenNamespace = true;
|
||||
return;
|
||||
}
|
||||
if (name === "import") {
|
||||
if (hasSeenNonImportRule ||
|
||||
hasSeenNamespace ||
|
||||
hasSeenLayerBlock) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidImportPlacement",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
hasSeenNonImportRule = true;
|
||||
},
|
||||
Rule() {
|
||||
hasSeenNonImportRule = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let fixable: "code";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unknownAtRule: string;
|
||||
let invalidPrelude: string;
|
||||
let unknownDescriptor: string;
|
||||
let invalidDescriptor: string;
|
||||
let invalidExtraPrelude: string;
|
||||
let missingPrelude: string;
|
||||
let invalidCharsetSyntax: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidAtRulesMessageIds;
|
||||
}>): {
|
||||
Atrule(node: AtrulePlain): void;
|
||||
"AtRule > Block > Declaration"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidAtRulesMessageIds = "unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude" | "invalidCharsetSyntax";
|
||||
export type NoInvalidAtRulesRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoInvalidAtRulesMessageIds;
|
||||
}>;
|
||||
import type { AtrulePlain } from "@eslint/css-tree";
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent the use of unknown at-rules in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { isSyntaxMatchError } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { AtrulePlain } from "@eslint/css-tree"
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude" | "invalidCharsetSyntax"} NoInvalidAtRulesMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulesMessageIds }>} NoInvalidAtRulesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Set of at-rules that can be nested inside style rules.
|
||||
* @see https://www.w3.org/TR/css-nesting-1/#conditionals
|
||||
*/
|
||||
const nestableAtRules = new Set([
|
||||
"media",
|
||||
"supports",
|
||||
"layer",
|
||||
"scope",
|
||||
"container",
|
||||
"starting-style",
|
||||
]);
|
||||
/**
|
||||
* A valid `@charset` rule must:
|
||||
* - Enclose the encoding name in double quotes
|
||||
* - Include exactly one space character after `@charset`
|
||||
* - End immediately with a semicolon
|
||||
*/
|
||||
const charsetPattern = /^@charset "[^"]+";$/u;
|
||||
const charsetEncodingPattern = /^['"]?([^"';]+)['"]?/u;
|
||||
/**
|
||||
* Extracts metadata from an error object.
|
||||
* @param {SyntaxError} error The error object to extract metadata from.
|
||||
* @returns {Object} The metadata extracted from the error.
|
||||
*/
|
||||
function extractMetaDataFromError(error) {
|
||||
const message = error.message;
|
||||
const atRuleName = /`@(.*)`/u.exec(message)[1];
|
||||
let messageId = "unknownAtRule";
|
||||
if (message.endsWith("prelude")) {
|
||||
messageId = message.includes("should not")
|
||||
? "invalidExtraPrelude"
|
||||
: "missingPrelude";
|
||||
}
|
||||
return {
|
||||
messageId,
|
||||
data: {
|
||||
name: atRuleName,
|
||||
},
|
||||
};
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidAtRulesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
fixable: "code",
|
||||
docs: {
|
||||
description: "Disallow invalid at-rules",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-at-rules.md",
|
||||
},
|
||||
messages: {
|
||||
unknownAtRule: "Unknown at-rule '@{{name}}' found.",
|
||||
invalidPrelude: "Invalid prelude '{{prelude}}' found for at-rule '@{{name}}'. Expected '{{expected}}'.",
|
||||
unknownDescriptor: "Unknown descriptor '{{descriptor}}' found for at-rule '@{{name}}'.",
|
||||
invalidDescriptor: "Invalid value '{{value}}' for descriptor '{{descriptor}}' found for at-rule '@{{name}}'. Expected {{expected}}.",
|
||||
invalidExtraPrelude: "At-rule '@{{name}}' should not contain a prelude.",
|
||||
missingPrelude: "At-rule '@{{name}}' should contain a prelude.",
|
||||
invalidCharsetSyntax: "Invalid @charset syntax. Expected '@charset \"{{encoding}}\";'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const lexer = sourceCode.lexer;
|
||||
/**
|
||||
* Validates a `@charset` rule for correct syntax:
|
||||
* - Verifies the rule name is exactly "charset" (case-sensitive)
|
||||
* - Ensures the rule has a prelude
|
||||
* - Validates the prelude matches the expected pattern
|
||||
* @param {AtrulePlain} node The node representing the rule.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateCharsetRule(node) {
|
||||
const { name, prelude, loc } = node;
|
||||
const charsetNameLoc = {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + name.length + 1,
|
||||
},
|
||||
};
|
||||
if (name !== "charset") {
|
||||
context.report({
|
||||
loc: charsetNameLoc,
|
||||
messageId: "unknownAtRule",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([
|
||||
loc.start.offset,
|
||||
loc.start.offset + name.length + 1,
|
||||
], "@charset");
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!prelude) {
|
||||
context.report({
|
||||
loc: charsetNameLoc,
|
||||
messageId: "missingPrelude",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const nodeText = sourceCode.getText(node);
|
||||
const preludeText = sourceCode.getText(prelude);
|
||||
const encoding = preludeText
|
||||
.match(charsetEncodingPattern)?.[1]
|
||||
?.trim();
|
||||
if (!encoding) {
|
||||
context.report({
|
||||
loc: prelude.loc,
|
||||
messageId: "invalidCharsetSyntax",
|
||||
data: { encoding: "<charset>" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!charsetPattern.test(nodeText)) {
|
||||
context.report({
|
||||
loc: prelude.loc,
|
||||
messageId: "invalidCharsetSyntax",
|
||||
data: { encoding },
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, `@charset "${encoding}";`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
Atrule(node) {
|
||||
if (node.name.toLowerCase() === "charset") {
|
||||
validateCharsetRule(node);
|
||||
return;
|
||||
}
|
||||
// checks both name and prelude
|
||||
const { error } = lexer.matchAtrulePrelude(node.name, node.prelude);
|
||||
if (error) {
|
||||
if (isSyntaxMatchError(error)) {
|
||||
context.report({
|
||||
loc: error.loc,
|
||||
messageId: "invalidPrelude",
|
||||
data: {
|
||||
name: node.name,
|
||||
prelude: error.css,
|
||||
expected: error.syntax,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loc = node.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
// add 1 to account for the @ symbol
|
||||
column: loc.start.column + node.name.length + 1,
|
||||
},
|
||||
},
|
||||
...extractMetaDataFromError(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
"AtRule > Block > Declaration"(node) {
|
||||
// skip custom descriptors
|
||||
if (node.property.startsWith("--")) {
|
||||
return;
|
||||
}
|
||||
// get at rule node
|
||||
const atRule = /** @type {AtrulePlain} */ (sourceCode.getParent(sourceCode.getParent(node)));
|
||||
if (nestableAtRules.has(atRule.name.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
const { error } = lexer.matchAtruleDescriptor(atRule.name, node.property, node.value);
|
||||
if (error) {
|
||||
if (isSyntaxMatchError(error)) {
|
||||
context.report({
|
||||
loc: error.loc,
|
||||
messageId: "invalidDescriptor",
|
||||
data: {
|
||||
name: atRule.name,
|
||||
descriptor: node.property,
|
||||
value: error.css,
|
||||
expected: error.syntax,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loc = node.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + node.property.length,
|
||||
},
|
||||
},
|
||||
messageId: "unknownDescriptor",
|
||||
data: {
|
||||
name: atRule.name,
|
||||
descriptor: node.property,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyGridArea: string;
|
||||
let unevenGridArea: string;
|
||||
let nonRectangularGridArea: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidNamedGridAreasMessageIds;
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidNamedGridAreasMessageIds = "emptyGridArea" | "unevenGridArea" | "nonRectangularGridArea";
|
||||
export type NoInvalidNamedGridAreasRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoInvalidNamedGridAreasMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent invalid named grid areas in CSS grid templates.
|
||||
* @author xbinaryx
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"emptyGridArea" | "unevenGridArea" | "nonRectangularGridArea"} NoInvalidNamedGridAreasMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidNamedGridAreasMessageIds }>} NoInvalidNamedGridAreasRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Regular expression to match null cell tokens (sequences of one or more dots)
|
||||
*/
|
||||
const nullCellToken = /^\.+$/u;
|
||||
/**
|
||||
* Finds non-rectangular grid areas in a 2D grid
|
||||
* @param {string[][]} grid 2D array representing the grid areas
|
||||
* @returns {Array<{name: string, row: number}>} Array of errors found
|
||||
*/
|
||||
function findNonRectangularAreas(grid) {
|
||||
const errors = [];
|
||||
const reported = new Set();
|
||||
const names = [...new Set(grid.flat())].filter(name => !nullCellToken.test(name));
|
||||
for (const name of names) {
|
||||
const indicesByRow = grid.map(row => {
|
||||
const indices = [];
|
||||
let idx = row.indexOf(name);
|
||||
while (idx !== -1) {
|
||||
indices.push(idx);
|
||||
idx = row.indexOf(name, idx + 1);
|
||||
}
|
||||
return indices;
|
||||
});
|
||||
for (let i = 0; i < indicesByRow.length; i++) {
|
||||
for (let j = i + 1; j < indicesByRow.length; j++) {
|
||||
const row1 = indicesByRow[i];
|
||||
const row2 = indicesByRow[j];
|
||||
if (row1.length === 0 || row2.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (row1.length !== row2.length ||
|
||||
!row1.every((val, idx) => val === row2[idx])) {
|
||||
const key = `${name}|${j}`;
|
||||
if (!reported.has(key)) {
|
||||
errors.push({ name, row: j });
|
||||
reported.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
const validProps = new Set(["grid-template-areas", "grid-template", "grid"]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidNamedGridAreasRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow invalid named grid areas",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-named-grid-areas.md",
|
||||
},
|
||||
messages: {
|
||||
emptyGridArea: "Grid area must contain at least one cell token.",
|
||||
unevenGridArea: "Grid area strings must have the same number of cell tokens.",
|
||||
nonRectangularGridArea: "Cell tokens with name '{{name}}' must form a rectangle.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Declaration(node) {
|
||||
const propName = node.property.toLowerCase();
|
||||
if (validProps.has(propName) &&
|
||||
node.value.type === "Value" &&
|
||||
node.value.children.length > 0) {
|
||||
const stringNodes = node.value.children.filter(child => child.type === "String");
|
||||
if (stringNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const grid = [];
|
||||
const emptyNodes = [];
|
||||
const unevenNodes = [];
|
||||
let firstRowLen = null;
|
||||
for (const stringNode of stringNodes) {
|
||||
const trimmedValue = stringNode.value.trim();
|
||||
if (trimmedValue === "") {
|
||||
emptyNodes.push(stringNode);
|
||||
continue;
|
||||
}
|
||||
const row = trimmedValue.split(" ").filter(Boolean);
|
||||
grid.push(row);
|
||||
if (firstRowLen === null) {
|
||||
firstRowLen = row.length;
|
||||
}
|
||||
else if (row.length !== firstRowLen) {
|
||||
unevenNodes.push(stringNode);
|
||||
}
|
||||
}
|
||||
if (emptyNodes.length > 0) {
|
||||
emptyNodes.forEach(emptyNode => context.report({
|
||||
node: emptyNode,
|
||||
messageId: "emptyGridArea",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (unevenNodes.length > 0) {
|
||||
unevenNodes.forEach(unevenNode => context.report({
|
||||
node: unevenNode,
|
||||
messageId: "unevenGridArea",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const nonRectErrors = findNonRectangularAreas(grid);
|
||||
nonRectErrors.forEach(({ name, row }) => {
|
||||
const stringNode = stringNodes[row];
|
||||
context.report({
|
||||
node: stringNode,
|
||||
messageId: "nonRectangularGridArea",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowUnknownVariables: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowUnknownVariables: false;
|
||||
}];
|
||||
namespace messages {
|
||||
let invalidPropertyValue: string;
|
||||
let unknownProperty: string;
|
||||
let unknownVar: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: NoInvalidPropertiesOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidPropertiesMessageIds;
|
||||
}>): {
|
||||
"Rule > Block Declaration"(): void;
|
||||
"Rule > Block Declaration > Value > *:not(Function)"(node: any): void;
|
||||
Function(): void;
|
||||
"Function > *:not(Function)"(node: any): void;
|
||||
"Function:exit"(node: FunctionNodePlain): void;
|
||||
"Rule > Block Declaration:exit"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidPropertiesMessageIds = "invalidPropertyValue" | "unknownProperty" | "unknownVar";
|
||||
export type NoInvalidPropertiesOptions = [{
|
||||
allowUnknownVariables?: boolean;
|
||||
}];
|
||||
export type NoInvalidPropertiesRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: NoInvalidPropertiesOptions;
|
||||
MessageIds: NoInvalidPropertiesMessageIds;
|
||||
}>;
|
||||
import type { FunctionNodePlain } from "@eslint/css-tree";
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent invalid properties in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @import { ValuePlain, FunctionNodePlain, CssLocationRange } from "@eslint/css-tree";
|
||||
* @typedef {"invalidPropertyValue" | "unknownProperty" | "unknownVar"} NoInvalidPropertiesMessageIds
|
||||
* @typedef {[{allowUnknownVariables?: boolean}]} NoInvalidPropertiesOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: NoInvalidPropertiesOptions, MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Regex to match var() functional notation with optional fallback.
|
||||
*/
|
||||
const varFunctionPattern = /var\(\s*(--[^,\s)]+)\s*(?:,([\s\S]+))?\)/iu;
|
||||
/**
|
||||
* Parses a var() function text and extracts the custom property name and fallback.
|
||||
* @param {string} text The text containing a var() function.
|
||||
* @returns {{ name: string, fallbackText: string | null } | null} The parsed variable name and optional fallback, or null if not a var().
|
||||
*/
|
||||
function parseVarFunction(text) {
|
||||
const match = text.match(varFunctionPattern);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: match[1].trim(),
|
||||
fallbackText: match[2]?.trim(),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Extracts the list of fallback value or variable name used in a `var()` that is used as fallback function.
|
||||
* For example, for `var(--my-color, var(--fallback-color, red));` it will return `["--fallback-color", "red"]`.
|
||||
* @param {string} value The fallback value that is used in `var()`.
|
||||
* @returns {Array<string>} The list of variable names of fallback value.
|
||||
*/
|
||||
function getVarFallbackList(value) {
|
||||
const list = [];
|
||||
let currentValue = value;
|
||||
while (true) {
|
||||
const parsed = parseVarFunction(currentValue);
|
||||
if (!parsed) {
|
||||
break;
|
||||
}
|
||||
list.push(parsed.name);
|
||||
if (!parsed.fallbackText) {
|
||||
break;
|
||||
}
|
||||
// If fallback is not another var(), we're done
|
||||
if (!parsed.fallbackText.toLowerCase().includes("var(")) {
|
||||
list.push(parsed.fallbackText);
|
||||
break;
|
||||
}
|
||||
// Continue parsing from fallback
|
||||
currentValue = parsed.fallbackText;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidPropertiesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow invalid properties",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-properties.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowUnknownVariables: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowUnknownVariables: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
invalidPropertyValue: "Invalid value '{{value}}' for property '{{property}}'. Expected {{expected}}.",
|
||||
unknownProperty: "Unknown property '{{property}}' found.",
|
||||
unknownVar: "Can't validate with unknown variable '{{var}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const lexer = sourceCode.lexer;
|
||||
/** @type {Map<string,ValuePlain>} */
|
||||
const vars = new Map();
|
||||
/**
|
||||
* @type {Array<{
|
||||
* valueParts: string[],
|
||||
* functionPartsStack: string[][],
|
||||
* valueSegmentLocs: Map<string,CssLocationRange>,
|
||||
* skipValidation: boolean,
|
||||
* hadVarSubstitution: boolean,
|
||||
* resolvedCache: Map<string,string>
|
||||
* }>}
|
||||
*/
|
||||
const declStack = [];
|
||||
const [{ allowUnknownVariables }] = context.options;
|
||||
/**
|
||||
* Iteratively resolves CSS variable references until a value is found.
|
||||
* @param {string} variableName The variable name to resolve
|
||||
* @param {Map<string, string>} cache Cache for memoization within a single resolution scope
|
||||
* @param {Set<string>} [seen] Set of already seen variables to detect cycles
|
||||
* @returns {string|null} The resolved value or null if not found
|
||||
*/
|
||||
function resolveVariable(variableName, cache, seen = new Set()) {
|
||||
/** @type {Array<string>} */
|
||||
const fallbackStack = [];
|
||||
let currentVarName = variableName;
|
||||
/*
|
||||
* Resolves a CSS variable by following its reference chain.
|
||||
*
|
||||
* Phase 1: Follow var() references
|
||||
* - Use `seen` to detect cycles
|
||||
* - Use `cache` for memoization
|
||||
* - If value is concrete: cache and return
|
||||
* - If value is another var(--next, <fallback>):
|
||||
* push fallback to stack and continue with --next
|
||||
* - If variable unknown: proceed to Phase 2
|
||||
*
|
||||
* Phase 2: Try fallback values (if Phase 1 failed)
|
||||
* - Process fallbacks in reverse order (LIFO)
|
||||
* - Resolve each via resolveFallback()
|
||||
* - Return first successful resolution
|
||||
*/
|
||||
while (true) {
|
||||
if (seen.has(currentVarName)) {
|
||||
break;
|
||||
}
|
||||
seen.add(currentVarName);
|
||||
if (cache.has(currentVarName)) {
|
||||
return cache.get(currentVarName);
|
||||
}
|
||||
const valueNode = vars.get(currentVarName);
|
||||
if (!valueNode) {
|
||||
break;
|
||||
}
|
||||
const valueText = sourceCode.getText(valueNode).trim();
|
||||
const parsed = parseVarFunction(valueText);
|
||||
if (!parsed) {
|
||||
cache.set(currentVarName, valueText);
|
||||
return valueText;
|
||||
}
|
||||
if (parsed.fallbackText) {
|
||||
fallbackStack.push(parsed.fallbackText);
|
||||
}
|
||||
currentVarName = parsed.name;
|
||||
}
|
||||
while (fallbackStack.length > 0) {
|
||||
const fallbackText = fallbackStack.pop();
|
||||
// eslint-disable-next-line no-use-before-define -- resolveFallback and resolveVariable are mutually recursive
|
||||
const resolvedFallback = resolveFallback(fallbackText, cache, seen);
|
||||
if (resolvedFallback !== null) {
|
||||
return resolvedFallback;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Resolves a fallback text which can contain nested var() calls.
|
||||
* Returns the first resolvable value or null if none resolve.
|
||||
* @param {string} rawFallbackText The raw fallback text to resolve.
|
||||
* @param {Map<string, string>} cache Cache for memoization within a single resolution scope.
|
||||
* @param {Set<string>} [seen] Set of already seen variables to detect cycles.
|
||||
* @returns {string | null} The resolved fallback value, or null if none can be resolved.
|
||||
*/
|
||||
function resolveFallback(rawFallbackText, cache, seen = new Set()) {
|
||||
const fallbackVarList = getVarFallbackList(rawFallbackText);
|
||||
if (fallbackVarList.length === 0) {
|
||||
return rawFallbackText;
|
||||
}
|
||||
for (const fallbackCandidate of fallbackVarList) {
|
||||
if (fallbackCandidate.startsWith("--")) {
|
||||
const resolved = resolveVariable(fallbackCandidate, cache, seen);
|
||||
if (resolved !== null) {
|
||||
return resolved;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return fallbackCandidate.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Process a var function node and add its resolved value to the value list
|
||||
* @param {Object} varNode The var() function node
|
||||
* @param {string[]} valueList Array to collect processed values
|
||||
* @param {Map<string,CssLocationRange>} valueSegmentLocs Map of rebuilt value segments to their locations
|
||||
* @param {Map<string, string>} resolvedCache Cache for resolved variable values to prevent redundant lookups
|
||||
* @returns {boolean} Whether processing was successful
|
||||
*/
|
||||
function processVarFunction(varNode, valueList, valueSegmentLocs, resolvedCache) {
|
||||
const varValue = vars.get(varNode.children[0].name);
|
||||
if (varValue) {
|
||||
const resolvedValue = resolveVariable(varNode.children[0].name, resolvedCache);
|
||||
if (resolvedValue) {
|
||||
valueList.push(resolvedValue);
|
||||
valueSegmentLocs.set(resolvedValue, varNode.loc);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If the variable is not found and doesn't have a fallback value, report it
|
||||
if (varNode.children.length === 1) {
|
||||
if (!allowUnknownVariables) {
|
||||
context.report({
|
||||
loc: varNode.children[0].loc,
|
||||
messageId: "unknownVar",
|
||||
data: { var: varNode.children[0].name },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Handle fallback values
|
||||
if (varNode.children[2].type !== "Raw") {
|
||||
return true;
|
||||
}
|
||||
const fallbackValue = varNode.children[2].value.trim();
|
||||
const resolvedFallbackValue = resolveFallback(fallbackValue, resolvedCache);
|
||||
if (resolvedFallbackValue) {
|
||||
valueList.push(resolvedFallbackValue);
|
||||
valueSegmentLocs.set(resolvedFallbackValue, varNode.loc);
|
||||
return true;
|
||||
}
|
||||
// No valid fallback found
|
||||
if (!allowUnknownVariables) {
|
||||
context.report({
|
||||
loc: varNode.children[0].loc,
|
||||
messageId: "unknownVar",
|
||||
data: { var: varNode.children[0].name },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return {
|
||||
"Rule > Block Declaration"() {
|
||||
declStack.push({
|
||||
valueParts: [],
|
||||
functionPartsStack: [],
|
||||
valueSegmentLocs: new Map(),
|
||||
skipValidation: false,
|
||||
hadVarSubstitution: false,
|
||||
/**
|
||||
* Cache for resolved variable values within this single declaration.
|
||||
* Prevents re-resolving the same variable and re-walking long `var()` chains.
|
||||
*/
|
||||
resolvedCache: new Map(),
|
||||
});
|
||||
},
|
||||
"Rule > Block Declaration > Value > *:not(Function)"(node) {
|
||||
const state = declStack.at(-1);
|
||||
const text = sourceCode.getText(node).trim();
|
||||
state.valueParts.push(text);
|
||||
state.valueSegmentLocs.set(text, node.loc);
|
||||
},
|
||||
Function() {
|
||||
const state = declStack.at(-1);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
state.functionPartsStack.push([]);
|
||||
},
|
||||
"Function > *:not(Function)"(node) {
|
||||
const state = declStack.at(-1);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const parts = state.functionPartsStack.at(-1);
|
||||
const text = sourceCode.getText(node).trim();
|
||||
parts.push(text);
|
||||
state.valueSegmentLocs.set(text, node.loc);
|
||||
},
|
||||
"Function:exit"(node) {
|
||||
const state = declStack.at(-1);
|
||||
if (!state || state.skipValidation) {
|
||||
return;
|
||||
}
|
||||
const parts = state.functionPartsStack.pop();
|
||||
let result;
|
||||
if (node.name.toLowerCase() === "var") {
|
||||
const resolvedParts = [];
|
||||
const success = processVarFunction(node, resolvedParts, state.valueSegmentLocs, state.resolvedCache);
|
||||
if (!success) {
|
||||
state.skipValidation = true;
|
||||
return;
|
||||
}
|
||||
if (resolvedParts.length === 0) {
|
||||
return;
|
||||
}
|
||||
state.hadVarSubstitution = true;
|
||||
result = resolvedParts[0];
|
||||
}
|
||||
else {
|
||||
result = `${node.name}(${parts.join(" ")})`;
|
||||
}
|
||||
const parentParts = state.functionPartsStack.at(-1);
|
||||
if (parentParts) {
|
||||
parentParts.push(result);
|
||||
}
|
||||
else {
|
||||
state.valueParts.push(result);
|
||||
}
|
||||
},
|
||||
"Rule > Block Declaration:exit"(node) {
|
||||
const state = declStack.pop();
|
||||
if (node.property.startsWith("--")) {
|
||||
// store the custom property name and value to validate later
|
||||
vars.set(node.property, node.value);
|
||||
// don't validate custom properties
|
||||
return;
|
||||
}
|
||||
if (state.skipValidation) {
|
||||
return;
|
||||
}
|
||||
let value = node.value;
|
||||
if (state.hadVarSubstitution) {
|
||||
const valueList = state.valueParts;
|
||||
value =
|
||||
valueList.length > 0
|
||||
? valueList.join(" ")
|
||||
: sourceCode.getText(node.value);
|
||||
}
|
||||
const { error } = lexer.matchProperty(node.property, value);
|
||||
if (error) {
|
||||
// validation failure
|
||||
if (isSyntaxMatchError(error)) {
|
||||
const errorValue = state.hadVarSubstitution &&
|
||||
value.slice(error.mismatchOffset, error.mismatchOffset + error.mismatchLength);
|
||||
context.report({
|
||||
/*
|
||||
* When using variables, check to see if the error
|
||||
* occurred at a location where a variable was replaced.
|
||||
* If so, use that location; otherwise, use the error's
|
||||
* reported location.
|
||||
*/
|
||||
loc: state.hadVarSubstitution
|
||||
? (state.valueSegmentLocs.get(errorValue) ??
|
||||
node.value.loc)
|
||||
: error.loc,
|
||||
messageId: "invalidPropertyValue",
|
||||
data: {
|
||||
property: node.property,
|
||||
/*
|
||||
* When using variables, slice the value to
|
||||
* only include the part that caused the error.
|
||||
* Otherwise, use the full value from the error.
|
||||
*/
|
||||
value: state.hadVarSubstitution
|
||||
? errorValue
|
||||
: error.css,
|
||||
expected: error.syntax,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!allowUnknownVariables ||
|
||||
isSyntaxReferenceError(error)) {
|
||||
// unknown property
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.loc.start,
|
||||
end: {
|
||||
line: node.loc.start.line,
|
||||
column: node.loc.start.column +
|
||||
node.property.length,
|
||||
},
|
||||
},
|
||||
messageId: "unknownProperty",
|
||||
data: {
|
||||
property: node.property,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unmatchableSelector: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "unmatchableSelector";
|
||||
}>): {
|
||||
AnPlusB(node: import("@eslint/css-tree").AnPlusB): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoUnmatchableSelectorsMessageIds = "unmatchableSelector";
|
||||
export type NoUnmatchableSelectorsRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoUnmatchableSelectorsMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow unmatchable selectors.
|
||||
* @author TKDev7
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"unmatchableSelector"} NoUnmatchableSelectorsMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoUnmatchableSelectorsMessageIds }>} NoUnmatchableSelectorsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoUnmatchableSelectorsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow unmatchable selectors",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-unmatchable-selectors.md",
|
||||
},
|
||||
messages: {
|
||||
unmatchableSelector: "Unexpected unmatchable selector '{{selector}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
AnPlusB(node) {
|
||||
// Either node.a or node.b can be null; Number(null) === 0.
|
||||
// This coercion is intentional so that omitted coefficients are treated as 0.
|
||||
const a = Number(node.a);
|
||||
const b = Number(node.b);
|
||||
if (a <= 0 && b <= 0) {
|
||||
const pseudo = sourceCode.getParent(sourceCode.getParent(node));
|
||||
context.report({
|
||||
loc: pseudo.loc,
|
||||
messageId: "unmatchableSelector",
|
||||
data: { selector: sourceCode.getText(pseudo) },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let hasSuggestions: true;
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowProperties: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowUnits: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowProperties: any[];
|
||||
allowUnits: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let notLogicalProperty: string;
|
||||
let notLogicalValue: string;
|
||||
let notLogicalUnit: string;
|
||||
let replaceWithLogicalProperty: string;
|
||||
let replaceWithLogicalValue: string;
|
||||
let replaceWithLogicalUnit: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: PreferLogicalPropertiesOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: PreferLogicalPropertiesMessageIds;
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
Dimension(node: import("@eslint/css-tree").Dimension): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type PreferLogicalPropertiesMessageIds = "notLogicalProperty" | "notLogicalValue" | "notLogicalUnit" | "replaceWithLogicalProperty" | "replaceWithLogicalValue" | "replaceWithLogicalUnit";
|
||||
export type PreferLogicalPropertiesOptions = [{
|
||||
allowProperties?: string[];
|
||||
allowUnits?: string[];
|
||||
}];
|
||||
export type PreferLogicalPropertiesRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: PreferLogicalPropertiesOptions;
|
||||
MessageIds: PreferLogicalPropertiesMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"notLogicalProperty" | "notLogicalValue" | "notLogicalUnit" | "replaceWithLogicalProperty" | "replaceWithLogicalValue" | "replaceWithLogicalUnit"} PreferLogicalPropertiesMessageIds
|
||||
* @typedef {[{
|
||||
* allowProperties?: string[],
|
||||
* allowUnits?: string[]
|
||||
* }]} PreferLogicalPropertiesOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: PreferLogicalPropertiesOptions, MessageIds: PreferLogicalPropertiesMessageIds }>} PreferLogicalPropertiesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const propertiesReplacements = new Map([
|
||||
["bottom", "inset-block-end"],
|
||||
["border-bottom", "border-block-end"],
|
||||
["border-bottom-color", "border-block-end-color"],
|
||||
["border-bottom-left-radius", "border-end-start-radius"],
|
||||
["border-bottom-right-radius", "border-end-end-radius"],
|
||||
["border-bottom-style", "border-block-end-style"],
|
||||
["border-bottom-width", "border-block-end-width"],
|
||||
["border-left", "border-inline-start"],
|
||||
["border-left-color", "border-inline-start-color"],
|
||||
["border-left-style", "border-inline-start-style"],
|
||||
["border-left-width", "border-inline-start-width"],
|
||||
["border-right", "border-inline-end"],
|
||||
["border-right-color", "border-inline-end-color"],
|
||||
["border-right-style", "border-inline-end-style"],
|
||||
["border-right-width", "border-inline-end-width"],
|
||||
["border-top", "border-block-start"],
|
||||
["border-top-color", "border-block-start-color"],
|
||||
["border-top-left-radius", "border-start-start-radius"],
|
||||
["border-top-right-radius", "border-start-end-radius"],
|
||||
["border-top-style", "border-block-start-style"],
|
||||
["border-top-width", "border-block-start-width"],
|
||||
["contain-intrinsic-height", "contain-intrinsic-block-size"],
|
||||
["contain-intrinsic-width", "contain-intrinsic-inline-size"],
|
||||
["height", "block-size"],
|
||||
["left", "inset-inline-start"],
|
||||
["margin-bottom", "margin-block-end"],
|
||||
["margin-left", "margin-inline-start"],
|
||||
["margin-right", "margin-inline-end"],
|
||||
["margin-top", "margin-block-start"],
|
||||
["max-height", "max-block-size"],
|
||||
["max-width", "max-inline-size"],
|
||||
["min-height", "min-block-size"],
|
||||
["min-width", "min-inline-size"],
|
||||
["overflow-x", "overflow-inline"],
|
||||
["overflow-y", "overflow-block"],
|
||||
["overscroll-behavior-x", "overscroll-behavior-inline"],
|
||||
["overscroll-behavior-y", "overscroll-behavior-block"],
|
||||
["padding-bottom", "padding-block-end"],
|
||||
["padding-left", "padding-inline-start"],
|
||||
["padding-right", "padding-inline-end"],
|
||||
["padding-top", "padding-block-start"],
|
||||
["right", "inset-inline-end"],
|
||||
["scroll-margin-bottom", "scroll-margin-block-end"],
|
||||
["scroll-margin-left", "scroll-margin-inline-start"],
|
||||
["scroll-margin-right", "scroll-margin-inline-end"],
|
||||
["scroll-margin-top", "scroll-margin-block-start"],
|
||||
["scroll-padding-bottom", "scroll-padding-block-end"],
|
||||
["scroll-padding-left", "scroll-padding-inline-start"],
|
||||
["scroll-padding-right", "scroll-padding-inline-end"],
|
||||
["scroll-padding-top", "scroll-padding-block-start"],
|
||||
["top", "inset-block-start"],
|
||||
["width", "inline-size"],
|
||||
]);
|
||||
const propertyValuesReplacements = new Map([
|
||||
[
|
||||
"text-align",
|
||||
{
|
||||
left: "start",
|
||||
right: "end",
|
||||
},
|
||||
],
|
||||
[
|
||||
"resize",
|
||||
{
|
||||
horizontal: "inline",
|
||||
vertical: "block",
|
||||
},
|
||||
],
|
||||
[
|
||||
"caption-side",
|
||||
{
|
||||
left: "inline-start",
|
||||
right: "inline-end",
|
||||
},
|
||||
],
|
||||
[
|
||||
"box-orient",
|
||||
{
|
||||
horizontal: "inline-axis",
|
||||
vertical: "block-axis",
|
||||
},
|
||||
],
|
||||
[
|
||||
"float",
|
||||
{
|
||||
left: "inline-start",
|
||||
right: "inline-end",
|
||||
},
|
||||
],
|
||||
[
|
||||
"clear",
|
||||
{
|
||||
left: "inline-start",
|
||||
right: "inline-end",
|
||||
},
|
||||
],
|
||||
]);
|
||||
const unitReplacements = new Map([
|
||||
["cqh", "cqb"],
|
||||
["cqw", "cqi"],
|
||||
["dvh", "dvb"],
|
||||
["dvw", "dvi"],
|
||||
["lvh", "lvb"],
|
||||
["lvw", "lvi"],
|
||||
["svh", "svb"],
|
||||
["svw", "svi"],
|
||||
["vh", "vb"],
|
||||
["vw", "vi"],
|
||||
]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {PreferLogicalPropertiesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: "Enforce the use of logical properties",
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/prefer-logical-properties.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowProperties: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(propertiesReplacements.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowUnits: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(unitReplacements.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowProperties: [],
|
||||
allowUnits: [],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
notLogicalProperty: "Expected logical property '{{replacement}}' instead of '{{property}}'.",
|
||||
notLogicalValue: "Expected logical value '{{replacement}}' instead of '{{value}}'.",
|
||||
notLogicalUnit: "Expected logical unit '{{replacement}}' instead of '{{unit}}'.",
|
||||
replaceWithLogicalProperty: "Replace '{{property}}' with logical property '{{replacement}}'.",
|
||||
replaceWithLogicalValue: "Replace '{{value}}' with logical value '{{replacement}}'.",
|
||||
replaceWithLogicalUnit: "Replace '{{unit}}' with logical unit '{{replacement}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const [{ allowProperties, allowUnits }] = context.options;
|
||||
return {
|
||||
Declaration(node) {
|
||||
const parent = context.sourceCode.getParent(node);
|
||||
if (parent.type === "SupportsDeclaration") {
|
||||
return;
|
||||
}
|
||||
const propertyReplacement = propertiesReplacements.get(node.property);
|
||||
if (propertyReplacement &&
|
||||
!allowProperties.includes(node.property)) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "notLogicalProperty",
|
||||
data: {
|
||||
property: node.property,
|
||||
replacement: propertyReplacement,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: "replaceWithLogicalProperty",
|
||||
data: {
|
||||
property: node.property,
|
||||
replacement: propertyReplacement,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([
|
||||
node.loc.start.offset,
|
||||
node.loc.start.offset +
|
||||
node.property.length,
|
||||
], propertyReplacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const valueReplacements = propertyValuesReplacements.get(node.property);
|
||||
if (valueReplacements &&
|
||||
node.value.type === "Value" &&
|
||||
node.value.children[0].type === "Identifier") {
|
||||
const identifier = node.value.children[0];
|
||||
const nodeValue = identifier.name;
|
||||
const valueReplacement = valueReplacements[nodeValue];
|
||||
if (valueReplacement) {
|
||||
context.report({
|
||||
loc: identifier.loc,
|
||||
messageId: "notLogicalValue",
|
||||
data: {
|
||||
value: nodeValue,
|
||||
replacement: valueReplacement,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: "replaceWithLogicalValue",
|
||||
data: {
|
||||
value: nodeValue,
|
||||
replacement: valueReplacement,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(identifier, valueReplacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Dimension(node) {
|
||||
const unitReplacement = unitReplacements.get(node.unit);
|
||||
if (unitReplacement && !allowUnits.includes(node.unit)) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "notLogicalUnit",
|
||||
data: {
|
||||
unit: node.unit,
|
||||
replacement: unitReplacement,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: "replaceWithLogicalUnit",
|
||||
data: {
|
||||
unit: node.unit,
|
||||
replacement: unitReplacement,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, node.value + unitReplacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "suggestion";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowUnits: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowUnits: string[];
|
||||
}];
|
||||
namespace messages {
|
||||
let allowedFontUnits: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: RelativeFontUnitsOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "allowedFontUnits";
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type RelativeFontUnitsMessageIds = "allowedFontUnits";
|
||||
export type RelativeFontUnitsOptions = [{
|
||||
allowUnits?: string[];
|
||||
}];
|
||||
export type RelativeFontUnitsRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: RelativeFontUnitsOptions;
|
||||
MessageIds: RelativeFontUnitsMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @fileoverview Enforce the use of relative units for font size.
|
||||
* @author Tanuj Kanti
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"allowedFontUnits"} RelativeFontUnitsMessageIds
|
||||
* @typedef {[{allowUnits?: string[]}]} RelativeFontUnitsOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: RelativeFontUnitsOptions, MessageIds: RelativeFontUnitsMessageIds}>} RelativeFontUnitsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const relativeFontUnits = [
|
||||
"%",
|
||||
"cap",
|
||||
"ch",
|
||||
"em",
|
||||
"ex",
|
||||
"ic",
|
||||
"lh",
|
||||
"rcap",
|
||||
"rch",
|
||||
"rem",
|
||||
"rex",
|
||||
"ric",
|
||||
"rlh",
|
||||
];
|
||||
const disallowedFontSizeKeywords = new Set([
|
||||
"xx-small",
|
||||
"x-small",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"x-large",
|
||||
"xx-large",
|
||||
"xxx-large",
|
||||
"math",
|
||||
]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {RelativeFontUnitsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description: "Enforce the use of relative font units",
|
||||
recommended: false,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/relative-font-units.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowUnits: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: relativeFontUnits,
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowUnits: ["rem"],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
allowedFontUnits: "Use only allowed relative units for 'font-size' - {{allowedFontUnits}}.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const [{ allowUnits: allowedFontUnits }] = context.options;
|
||||
return {
|
||||
Declaration(node) {
|
||||
if (node.property === "font-size") {
|
||||
if (node.value.type === "Value" &&
|
||||
node.value.children.length > 0) {
|
||||
const value = node.value.children[0];
|
||||
if ((value.type === "Dimension" &&
|
||||
!allowedFontUnits.includes(value.unit.toLowerCase())) ||
|
||||
(value.type === "Identifier" &&
|
||||
disallowedFontSizeKeywords.has(value.name.toLowerCase())) ||
|
||||
(value.type === "Percentage" &&
|
||||
!allowedFontUnits.includes("%"))) {
|
||||
context.report({
|
||||
loc: value.loc,
|
||||
messageId: "allowedFontUnits",
|
||||
data: {
|
||||
allowedFontUnits: allowedFontUnits.join(", "),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.property === "font") {
|
||||
if (node.value.type === "Value" &&
|
||||
node.value.children.length > 0) {
|
||||
const value = node.value;
|
||||
const dimensionNode = value.children.find(child => child.type === "Dimension");
|
||||
const identifierNode = value.children.find(child => child.type === "Identifier" &&
|
||||
disallowedFontSizeKeywords.has(child.name.toLowerCase()));
|
||||
const percentageNode = value.children.find((child, index) => {
|
||||
const isPercentage = child.type === "Percentage";
|
||||
const previousNode = value.children[index - 1];
|
||||
const previousNodeIsSlashOperator = previousNode &&
|
||||
previousNode.type === "Operator" &&
|
||||
previousNode.value === "/";
|
||||
return (isPercentage && !previousNodeIsSlashOperator);
|
||||
});
|
||||
let location;
|
||||
let shouldReport = false;
|
||||
const conditions = [
|
||||
{
|
||||
check: !allowedFontUnits.includes("%") &&
|
||||
percentageNode,
|
||||
loc: percentageNode?.loc,
|
||||
},
|
||||
{
|
||||
check: identifierNode,
|
||||
loc: identifierNode?.loc,
|
||||
},
|
||||
{
|
||||
check: dimensionNode &&
|
||||
!allowedFontUnits.includes(dimensionNode.unit.toLowerCase()),
|
||||
loc: dimensionNode?.loc,
|
||||
},
|
||||
];
|
||||
for (const condition of conditions) {
|
||||
if (condition.check) {
|
||||
shouldReport = true;
|
||||
location = condition.loc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldReport) {
|
||||
context.report({
|
||||
loc: location,
|
||||
messageId: "allowedFontUnits",
|
||||
data: {
|
||||
allowedFontUnits: allowedFontUnits.join(", "),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
maxIds: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxClasses: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxTypes: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxAttributes: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxPseudoClasses: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxUniversals: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxCompounds: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxCombinators: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
disallowCombinators: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
disallowPseudoClasses: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
disallowPseudoElements: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
disallowAttributes: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
disallowAttributeMatchers: {
|
||||
type: "array";
|
||||
items: {
|
||||
type: "string";
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
maxIds: number;
|
||||
maxClasses: number;
|
||||
maxTypes: number;
|
||||
maxAttributes: number;
|
||||
maxPseudoClasses: number;
|
||||
maxUniversals: number;
|
||||
maxCompounds: number;
|
||||
maxCombinators: number;
|
||||
disallowCombinators: any[];
|
||||
disallowPseudoClasses: any[];
|
||||
disallowPseudoElements: any[];
|
||||
disallowAttributes: any[];
|
||||
disallowAttributeMatchers: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let maxSelectors: string;
|
||||
let disallowedSelectors: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: SelectorComplexityOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: SelectorComplexityMessageIds;
|
||||
}>): {
|
||||
Selector(node: import("@eslint/css-tree").SelectorPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type SelectorComplexityMessageIds = "maxSelectors" | "disallowedSelectors";
|
||||
export type SelectorComplexityOptions = [{
|
||||
maxIds?: number;
|
||||
maxClasses?: number;
|
||||
maxTypes?: number;
|
||||
maxAttributes?: number;
|
||||
maxPseudoClasses?: number;
|
||||
maxUniversals?: number;
|
||||
maxCompounds?: number;
|
||||
maxCombinators?: number;
|
||||
disallowCombinators?: string[];
|
||||
disallowPseudoClasses?: string[];
|
||||
disallowPseudoElements?: string[];
|
||||
disallowAttributes?: string[];
|
||||
disallowAttributeMatchers?: string[];
|
||||
}];
|
||||
export type SelectorComplexityRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: SelectorComplexityOptions;
|
||||
MessageIds: SelectorComplexityMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* @fileoverview Rule to limit and disallow CSS selectors.
|
||||
* @author Tanuj Kanti
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"maxSelectors" | "disallowedSelectors"} SelectorComplexityMessageIds
|
||||
* @typedef {[{
|
||||
* maxIds?: number,
|
||||
* maxClasses?: number,
|
||||
* maxTypes?: number,
|
||||
* maxAttributes?: number,
|
||||
* maxPseudoClasses?: number,
|
||||
* maxUniversals?: number,
|
||||
* maxCompounds?: number,
|
||||
* maxCombinators?: number,
|
||||
* disallowCombinators?: string[],
|
||||
* disallowPseudoClasses?: string[],
|
||||
* disallowPseudoElements?: string[],
|
||||
* disallowAttributes?: string[],
|
||||
* disallowAttributeMatchers?: string[],
|
||||
* }]} SelectorComplexityOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: SelectorComplexityOptions, MessageIds: SelectorComplexityMessageIds }> } SelectorComplexityRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Get the location of a selector of a given name.
|
||||
* @param {Array<Object>} allSelector All CSS selector nodes.
|
||||
* @param {string} disallowedSelector The name of the disallowed selector.
|
||||
* @returns {Object} The location of the disallowed selector.
|
||||
*/
|
||||
function getDisallowedSelectorsLocation(allSelector, disallowedSelector) {
|
||||
return allSelector.find(selector => selector.name === disallowedSelector)
|
||||
.loc;
|
||||
}
|
||||
/**
|
||||
* An error for exceeding the maximum allowed selectors of a specific type.
|
||||
* @param {Object} context The ESLint rule context object.
|
||||
* @param {Object} selectorLoc The location of the selector.
|
||||
* @param {number} maxValue The max number of selectors that are allowed.
|
||||
* @param {string} selectorType The type of CSS selector.
|
||||
* @returns {void}
|
||||
*/
|
||||
function exceedLimitError(context, selectorLoc, maxValue, selectorType) {
|
||||
context.report({
|
||||
loc: selectorLoc,
|
||||
messageId: "maxSelectors",
|
||||
data: {
|
||||
selector: selectorType,
|
||||
limit: maxValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Gives an array of CSS selectors of a specific type.
|
||||
* @param {Array<Object>} selectors All CSS selectors nodes.
|
||||
* @param {string} selectorType The type of CSS selector to filter out.
|
||||
* @returns {Array<Object>} Filtered selectors.
|
||||
*/
|
||||
function getSelectors(selectors, selectorType) {
|
||||
return selectors.filter(selector => selector.type === selectorType);
|
||||
}
|
||||
/**
|
||||
* Get the names of all CSS selectors.
|
||||
* @param {Array<Object>} selectors All CSS selector nodes.
|
||||
* @returns {Array<string>} Array of selector names.
|
||||
*/
|
||||
function getSelectorNames(selectors) {
|
||||
return selectors.map(selector => selector.name);
|
||||
}
|
||||
/**
|
||||
* Get the location of the attribute matcher or operator in a given attribute selector.
|
||||
* @param {Array<Object>} selectors All CSS selector nodes.
|
||||
* @param {number} index The index of the attribute selector in the selectors array.
|
||||
* @returns {{ startLoc: Object, endLoc: Object }} The start and end locations of the operator.
|
||||
*/
|
||||
function getOperatorLocation(selectors, index) {
|
||||
const selector = selectors[index];
|
||||
let startLoc;
|
||||
let endLoc;
|
||||
if (selector.name.type === "Identifier") {
|
||||
startLoc = selector.name.loc.end;
|
||||
}
|
||||
if (selector.value) {
|
||||
endLoc = selector.value.loc.start;
|
||||
}
|
||||
return { startLoc, endLoc };
|
||||
}
|
||||
/**
|
||||
* Get the location of a given disallowed combinator.
|
||||
* @param {Array<Object>} selectors All CSS selector nodes.
|
||||
* @param {Array<Object>} combinatorNodes All combinator nodes.
|
||||
* @param {string} combinator Name of combinator.
|
||||
* @param {number} index The index of the given combinator.
|
||||
* @returns {Object} The location of the disallowed combinator.
|
||||
*/
|
||||
function getDisallowedCombinatorsLocation(selectors, combinatorNodes, combinator, index) {
|
||||
let location;
|
||||
if (combinator === " ") {
|
||||
const selectorsArr = [];
|
||||
let selectorsGroup = [];
|
||||
selectors.forEach(selector => {
|
||||
if (selector.type === "Combinator") {
|
||||
selectorsArr.push(selectorsGroup);
|
||||
selectorsGroup = [];
|
||||
}
|
||||
else {
|
||||
selectorsGroup.push(selector);
|
||||
}
|
||||
});
|
||||
if (selectorsGroup.length > 0) {
|
||||
selectorsArr.push(selectorsGroup);
|
||||
}
|
||||
location = {
|
||||
start: selectorsArr[index].at(-1).loc.end,
|
||||
end: selectorsArr[index + 1][0].loc.start,
|
||||
};
|
||||
}
|
||||
else {
|
||||
const currentCombinatorNode = combinatorNodes[index];
|
||||
location = currentCombinatorNode.loc;
|
||||
}
|
||||
return location;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {SelectorComplexityRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow and limit CSS selectors",
|
||||
recommended: false,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/selector-complexity.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maxIds: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxClasses: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxTypes: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxAttributes: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxPseudoClasses: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxUniversals: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxCompounds: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
maxCombinators: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
},
|
||||
disallowCombinators: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
disallowPseudoClasses: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
disallowPseudoElements: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
disallowAttributes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
disallowAttributeMatchers: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
maxIds: Infinity,
|
||||
maxClasses: Infinity,
|
||||
maxTypes: Infinity,
|
||||
maxAttributes: Infinity,
|
||||
maxPseudoClasses: Infinity,
|
||||
maxUniversals: Infinity,
|
||||
maxCompounds: Infinity,
|
||||
maxCombinators: Infinity,
|
||||
disallowCombinators: [],
|
||||
disallowPseudoClasses: [],
|
||||
disallowPseudoElements: [],
|
||||
disallowAttributes: [],
|
||||
disallowAttributeMatchers: [],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
maxSelectors: "Exceeded maximum {{selector}} selector. Only {{limit}} allowed.",
|
||||
disallowedSelectors: "'{{selectorName}}' {{selector}} is not allowed.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const [{ maxIds, maxClasses, maxTypes, maxAttributes, maxPseudoClasses, maxUniversals, maxCompounds, maxCombinators, disallowCombinators, disallowPseudoClasses, disallowPseudoElements, disallowAttributes, disallowAttributeMatchers, },] = context.options;
|
||||
return {
|
||||
Selector(node) {
|
||||
const selectors = node.children;
|
||||
const selectorLoc = node.loc;
|
||||
const idSelectors = getSelectors(selectors, "IdSelector");
|
||||
const classSelectors = getSelectors(selectors, "ClassSelector");
|
||||
const typeSelectors = selectors.filter(child => child.type === "TypeSelector" && child.name !== "*");
|
||||
const attributeSelectors = getSelectors(selectors, "AttributeSelector");
|
||||
const pseudoClassSelectors = getSelectors(selectors, "PseudoClassSelector");
|
||||
const universalSelectors = selectors.filter(child => child.type === "TypeSelector" && child.name === "*");
|
||||
const combinatorNodes = getSelectors(selectors, "Combinator");
|
||||
const combinators = getSelectorNames(combinatorNodes);
|
||||
const pseudoClassSelectorsNames = getSelectorNames(pseudoClassSelectors);
|
||||
const pseudoElementSelectors = getSelectors(selectors, "PseudoElementSelector");
|
||||
const pseudoElementNames = getSelectorNames(pseudoElementSelectors);
|
||||
const attributeNames = attributeSelectors.map(s => s.name.name);
|
||||
const attributeMatchers = attributeSelectors
|
||||
.map(child => child.matcher)
|
||||
.filter(Boolean);
|
||||
if (idSelectors.length > maxIds) {
|
||||
exceedLimitError(context, selectorLoc, maxIds, "id");
|
||||
}
|
||||
if (classSelectors.length > maxClasses) {
|
||||
exceedLimitError(context, selectorLoc, maxClasses, "class");
|
||||
}
|
||||
if (typeSelectors.length > maxTypes) {
|
||||
exceedLimitError(context, selectorLoc, maxTypes, "type");
|
||||
}
|
||||
if (attributeSelectors.length > maxAttributes) {
|
||||
exceedLimitError(context, selectorLoc, maxAttributes, "attribute");
|
||||
}
|
||||
if (pseudoClassSelectors.length > maxPseudoClasses) {
|
||||
exceedLimitError(context, selectorLoc, maxPseudoClasses, "pseudo-class");
|
||||
}
|
||||
if (universalSelectors.length > maxUniversals) {
|
||||
exceedLimitError(context, selectorLoc, maxUniversals, "universal");
|
||||
}
|
||||
if (combinatorNodes.length > maxCombinators) {
|
||||
exceedLimitError(context, selectorLoc, maxCombinators, "combinator");
|
||||
}
|
||||
if (combinatorNodes.length + 1 > maxCompounds) {
|
||||
exceedLimitError(context, selectorLoc, maxCompounds, "compound");
|
||||
}
|
||||
if (disallowPseudoClasses.length > 0) {
|
||||
let disallowedPseudoClassLocation;
|
||||
for (const pseudoClassName of pseudoClassSelectorsNames) {
|
||||
if (disallowPseudoClasses.includes(pseudoClassName)) {
|
||||
disallowedPseudoClassLocation =
|
||||
getDisallowedSelectorsLocation(pseudoClassSelectors, pseudoClassName);
|
||||
context.report({
|
||||
loc: disallowedPseudoClassLocation,
|
||||
messageId: "disallowedSelectors",
|
||||
data: {
|
||||
selectorName: pseudoClassName,
|
||||
selector: "pseudo-class",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disallowCombinators.length > 0) {
|
||||
let disallowedCombinatorLocation;
|
||||
for (const [index, combinator] of combinators.entries()) {
|
||||
if (disallowCombinators.includes(combinator)) {
|
||||
disallowedCombinatorLocation =
|
||||
getDisallowedCombinatorsLocation(selectors, combinatorNodes, combinator, index);
|
||||
context.report({
|
||||
loc: disallowedCombinatorLocation,
|
||||
messageId: "disallowedSelectors",
|
||||
data: {
|
||||
selectorName: combinator,
|
||||
selector: "combinator",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disallowPseudoElements.length > 0) {
|
||||
let disallowPseudoElementsLocation;
|
||||
for (const pseudoElement of pseudoElementNames) {
|
||||
if (disallowPseudoElements.includes(pseudoElement)) {
|
||||
disallowPseudoElementsLocation =
|
||||
getDisallowedSelectorsLocation(pseudoElementSelectors, pseudoElement);
|
||||
context.report({
|
||||
loc: disallowPseudoElementsLocation,
|
||||
messageId: "disallowedSelectors",
|
||||
data: {
|
||||
selectorName: pseudoElement,
|
||||
selector: "pseudo-element",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disallowAttributes.length > 0) {
|
||||
let disallowAttributesLocation;
|
||||
for (const attributeName of attributeNames) {
|
||||
if (disallowAttributes.includes(attributeName)) {
|
||||
disallowAttributesLocation =
|
||||
attributeSelectors.find(selector => selector.name.name === attributeName).name.loc;
|
||||
context.report({
|
||||
loc: disallowAttributesLocation,
|
||||
messageId: "disallowedSelectors",
|
||||
data: {
|
||||
selectorName: attributeName,
|
||||
selector: "attribute",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disallowAttributeMatchers.length > 0) {
|
||||
for (const [index, attributeMatcher,] of attributeMatchers.entries()) {
|
||||
if (disallowAttributeMatchers.includes(attributeMatcher)) {
|
||||
const { startLoc, endLoc } = getOperatorLocation(attributeSelectors.filter(s => s.matcher), index);
|
||||
context.report({
|
||||
loc: {
|
||||
start: startLoc,
|
||||
end: endLoc,
|
||||
},
|
||||
messageId: "disallowedSelectors",
|
||||
data: {
|
||||
selectorName: attributeMatcher,
|
||||
selector: "attribute-matcher",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
available: {
|
||||
anyOf: ({
|
||||
enum: string[];
|
||||
type?: undefined;
|
||||
minimum?: undefined;
|
||||
maximum?: undefined;
|
||||
} | {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
enum?: undefined;
|
||||
})[];
|
||||
};
|
||||
allowAtRules: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowFunctions: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowMediaConditions: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowProperties: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowPropertyValues: {
|
||||
type: "object";
|
||||
properties: {
|
||||
[k: string]: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
};
|
||||
allowSelectors: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowUnits: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
available: "widely";
|
||||
allowAtRules: any[];
|
||||
allowFunctions: any[];
|
||||
allowMediaConditions: any[];
|
||||
allowProperties: any[];
|
||||
allowPropertyValues: {};
|
||||
allowSelectors: any[];
|
||||
allowUnits: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let notBaselineProperty: string;
|
||||
let notBaselinePropertyValue: string;
|
||||
let notBaselineAtRule: string;
|
||||
let notBaselineFunction: string;
|
||||
let notBaselineMediaCondition: string;
|
||||
let notBaselineSelector: string;
|
||||
let notBaselineUnit: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: UseBaselineOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: UseBaselineMessageIds;
|
||||
}>): {
|
||||
"Atrule[name=/^supports$/i]"(): void;
|
||||
"Atrule[name=/^supports$/i] > AtrulePrelude > Condition"(node: any): void;
|
||||
"Rule > Block > Declaration"(node: any): void;
|
||||
"Atrule[name=/^supports$/i]:exit"(): void;
|
||||
"Atrule[name=/^media$/i] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"(node: any): void;
|
||||
Atrule(node: import("@eslint/css-tree").AtrulePlain): void;
|
||||
"PseudoClassSelector,PseudoElementSelector"(node: any): void;
|
||||
NestingSelector(node: import("@eslint/css-tree").NestingSelector): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type UseBaselineMessageIds = "notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineFunction" | "notBaselineMediaCondition" | "notBaselineSelector" | "notBaselineUnit";
|
||||
export type UseBaselineOptions = [{
|
||||
available?: "widely" | "newly" | number;
|
||||
allowAtRules?: string[];
|
||||
allowFunctions?: string[];
|
||||
allowMediaConditions?: string[];
|
||||
allowProperties?: string[];
|
||||
allowPropertyValues?: {
|
||||
[property: string]: string[];
|
||||
};
|
||||
allowSelectors?: string[];
|
||||
allowUnits?: string[];
|
||||
}];
|
||||
export type UseBaselineRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: UseBaselineOptions;
|
||||
MessageIds: UseBaselineMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+868
@@ -0,0 +1,868 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce the use of baseline features.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { BASELINE_HIGH, BASELINE_LOW, properties, propertyValues, atRules, mediaConditions, functions, units, selectors, } from "../data/baseline-data.js";
|
||||
import { namedColors } from "../data/colors.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @import { Identifier, FunctionNodePlain, Dimension } from "@eslint/css-tree"
|
||||
* @typedef {"notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineFunction" | "notBaselineMediaCondition" | "notBaselineSelector" | "notBaselineUnit"} UseBaselineMessageIds
|
||||
* @typedef {[{
|
||||
* available?: "widely" | "newly" | number,
|
||||
* allowAtRules?: string[],
|
||||
* allowFunctions?: string[],
|
||||
* allowMediaConditions?: string[],
|
||||
* allowProperties?: string[],
|
||||
* allowPropertyValues?: { [property: string]: string[] },
|
||||
* allowSelectors?: string[],
|
||||
* allowUnits?: string[]
|
||||
* }]} UseBaselineOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: UseBaselineOptions, MessageIds: UseBaselineMessageIds }>} UseBaselineRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Represents a property that is supported via `@supports`.
|
||||
*/
|
||||
class SupportedProperty {
|
||||
/**
|
||||
* The name of the property.
|
||||
* @type {string}
|
||||
*/
|
||||
name;
|
||||
/**
|
||||
* Supported identifier values.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#identifiers = new Set();
|
||||
/**
|
||||
* Supported units.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#units = new Set();
|
||||
/**
|
||||
* Supported function types.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#functions = new Set();
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {string} name The name of the property.
|
||||
*/
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
/**
|
||||
* Adds an identifier to the list of supported identifiers.
|
||||
* @param {string} identifier The identifier to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addIdentifier(identifier) {
|
||||
this.#identifiers.add(identifier);
|
||||
}
|
||||
/**
|
||||
* Determines if an identifier is supported.
|
||||
* @param {string} identifier The identifier to check.
|
||||
* @returns {boolean} `true` if the identifier is supported, `false` if not.
|
||||
*/
|
||||
hasIdentifier(identifier) {
|
||||
return this.#identifiers.has(identifier);
|
||||
}
|
||||
/**
|
||||
* Determines if any identifiers are supported.
|
||||
* @returns {boolean} `true` if any identifiers are supported, `false` if not.
|
||||
*/
|
||||
hasIdentifiers() {
|
||||
return this.#identifiers.size > 0;
|
||||
}
|
||||
/**
|
||||
* Adds a unit to the list of supported units.
|
||||
* @param {string} unit The unit to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addUnit(unit) {
|
||||
this.#units.add(unit);
|
||||
}
|
||||
/**
|
||||
* Determines if a unit is supported.
|
||||
* @param {string} unit The unit to check.
|
||||
* @returns {boolean} `true` if the unit is supported, `false` if not.
|
||||
*/
|
||||
hasUnit(unit) {
|
||||
return this.#units.has(unit);
|
||||
}
|
||||
/**
|
||||
* Determines if any units are supported.
|
||||
* @returns {boolean} `true` if any units are supported, `false` if not.
|
||||
*/
|
||||
hasUnits() {
|
||||
return this.#units.size > 0;
|
||||
}
|
||||
/**
|
||||
* Adds a function to the list of supported functions.
|
||||
* @param {string} func The function to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addFunction(func) {
|
||||
this.#functions.add(func);
|
||||
}
|
||||
/**
|
||||
* Determines if a function is supported.
|
||||
* @param {string} func The function to check.
|
||||
* @returns {boolean} `true` if the function is supported, `false` if not.
|
||||
*/
|
||||
hasFunction(func) {
|
||||
return this.#functions.has(func);
|
||||
}
|
||||
/**
|
||||
* Determines if any functions are supported.
|
||||
* @returns {boolean} `true` if any functions are supported, `false` if not.
|
||||
*/
|
||||
hasFunctions() {
|
||||
return this.#functions.size > 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents an `@supports` rule and everything it enables.
|
||||
*/
|
||||
class SupportsRule {
|
||||
/**
|
||||
* The properties supported by this rule.
|
||||
* @type {Map<string, SupportedProperty>}
|
||||
*/
|
||||
#properties = new Map();
|
||||
/**
|
||||
* The selectors supported by this rule.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#selectors = new Set();
|
||||
/**
|
||||
* Adds a property to the rule.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {SupportedProperty} The supported property object.
|
||||
*/
|
||||
addProperty(property) {
|
||||
if (this.#properties.has(property)) {
|
||||
return this.#properties.get(property);
|
||||
}
|
||||
const supportedProperty = new SupportedProperty(property);
|
||||
this.#properties.set(property, supportedProperty);
|
||||
return supportedProperty;
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a property.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if the property is supported, `false` if not.
|
||||
*/
|
||||
hasProperty(property) {
|
||||
return this.#properties.has(property);
|
||||
}
|
||||
/**
|
||||
* Gets the supported property.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {SupportedProperty} The supported property.
|
||||
*/
|
||||
getProperty(property) {
|
||||
return this.#properties.get(property);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a property value.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} identifier The identifier to check.
|
||||
* @returns {boolean} `true` if the property value is supported, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifier(property, identifier) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasIdentifier(identifier);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports any property values.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any property values are supported, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifiers(property) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasIdentifiers();
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a function.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} func The function to check.
|
||||
* @returns {boolean} `true` if the function is supported, `false` if not.
|
||||
*/
|
||||
hasFunction(property, func) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasFunction(func);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports any functions.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any functions are supported, `false` if not.
|
||||
*/
|
||||
hasFunctions(property) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasFunctions();
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a unit.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} unit The unit to check.
|
||||
* @returns {boolean} `true` if the unit is supported, `false` if not.
|
||||
*/
|
||||
hasPropertyUnit(property, unit) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasUnit(unit);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports any units.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any units are supported, `false` if not.
|
||||
*/
|
||||
hasPropertyUnits(property) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasUnits();
|
||||
}
|
||||
/**
|
||||
* Adds a selector to the rule.
|
||||
* @param {string} selector The name of the selector.
|
||||
* @returns {void}
|
||||
*/
|
||||
addSelector(selector) {
|
||||
this.#selectors.add(selector);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a selector.
|
||||
* @param {string} selector The name of the selector.
|
||||
* @returns {boolean} `true` if the selector is supported, `false` if not.
|
||||
*/
|
||||
hasSelector(selector) {
|
||||
return this.#selectors.has(selector);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents a collection of supports rules.
|
||||
*/
|
||||
class SupportsRules {
|
||||
/**
|
||||
* A collection of supports rules.
|
||||
* @type {Array<SupportsRule>}
|
||||
*/
|
||||
#rules = [];
|
||||
/**
|
||||
* Adds a rule to the collection.
|
||||
* @param {SupportsRule} rule The rule to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
push(rule) {
|
||||
this.#rules.push(rule);
|
||||
}
|
||||
/**
|
||||
* Removes the last rule from the collection.
|
||||
* @returns {SupportsRule} The last rule in the collection.
|
||||
*/
|
||||
pop() {
|
||||
return this.#rules.pop();
|
||||
}
|
||||
/**
|
||||
* Retrieves the last rule in the collection.
|
||||
* @returns {SupportsRule} The last rule in the collection.
|
||||
*/
|
||||
last() {
|
||||
return this.#rules.at(-1);
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a property.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any rule supports the property, `false` if not.
|
||||
*/
|
||||
hasProperty(property) {
|
||||
return this.#rules.some(rule => rule.hasProperty(property));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a property identifier.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} identifier The identifier to check.
|
||||
* @returns {boolean} `true` if any rule supports the property value, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifier(property, identifier) {
|
||||
return this.#rules.some(rule => rule.hasPropertyIdentifier(property, identifier));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports any property identifiers.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any rule supports the property values, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifiers(property) {
|
||||
return this.#rules.some(rule => rule.hasPropertyIdentifiers(property));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a function.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} func The function to check.
|
||||
* @returns {boolean} `true` if any rule supports the function, `false` if not.
|
||||
*/
|
||||
hasPropertyFunction(property, func) {
|
||||
return this.#rules.some(rule => rule.hasFunction(property, func));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports any functions.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any rule supports the functions, `false` if not.
|
||||
*/
|
||||
hasPropertyFunctions(property) {
|
||||
return this.#rules.some(rule => rule.hasFunctions(property));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a unit.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} unit The unit to check.
|
||||
* @returns {boolean} `true` if any rule supports the unit, `false` if not.
|
||||
*/
|
||||
hasPropertyUnit(property, unit) {
|
||||
return this.#rules.some(rule => rule.hasPropertyUnit(property, unit));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a selector.
|
||||
* @param {string} selector The name of the selector.
|
||||
* @returns {boolean} `true` if any rule supports the selector, `false` if not.
|
||||
*/
|
||||
hasSelector(selector) {
|
||||
return this.#rules.some(rule => rule.hasSelector(selector));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents the required availability of a feature.
|
||||
*/
|
||||
class BaselineAvailability {
|
||||
/**
|
||||
* The preferred Baseline year.
|
||||
* @type {number}
|
||||
*/
|
||||
#baselineYear = undefined;
|
||||
/**
|
||||
* The preferred Baseline status.
|
||||
* @type {number}
|
||||
*/
|
||||
#baselineStatus = undefined;
|
||||
/**
|
||||
* @param {string | number} availability The required level of feature availability.
|
||||
*/
|
||||
constructor(availability) {
|
||||
this.availability = availability;
|
||||
if (typeof availability === "number") {
|
||||
this.#baselineYear = availability;
|
||||
}
|
||||
else {
|
||||
this.#baselineStatus =
|
||||
availability === "widely" ? BASELINE_HIGH : BASELINE_LOW;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines whether a feature meets the required availability.
|
||||
* @param {Object} encodedStatus A feature's encoded baseline status and year.
|
||||
* @returns {boolean} `true` if the feature is supported, `false` if not.
|
||||
*/
|
||||
isSupported(encodedStatus) {
|
||||
if (!encodedStatus) {
|
||||
// if we don't know the status, assume it's supported
|
||||
return true;
|
||||
}
|
||||
const parts = encodedStatus.split(":");
|
||||
const status = Number(parts[0]);
|
||||
const year = Number(parts[1] || NaN);
|
||||
if (this.#baselineYear) {
|
||||
return year <= this.#baselineYear;
|
||||
}
|
||||
return status >= this.#baselineStatus;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {UseBaselineRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Enforce the use of baseline features",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/use-baseline.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
available: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: ["widely", "newly"],
|
||||
},
|
||||
{
|
||||
// baseline year
|
||||
type: "integer",
|
||||
minimum: 2000,
|
||||
maximum: new Date().getFullYear(),
|
||||
},
|
||||
],
|
||||
},
|
||||
allowAtRules: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(atRules.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowFunctions: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(functions.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowMediaConditions: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(mediaConditions.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowProperties: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(properties.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowPropertyValues: {
|
||||
type: "object",
|
||||
properties: Object.fromEntries(Array.from(propertyValues.entries()).map(([prop, valuesMap]) => [
|
||||
prop,
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(valuesMap.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
])),
|
||||
additionalProperties: false,
|
||||
},
|
||||
allowSelectors: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(selectors.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowUnits: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(units.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
available: "widely",
|
||||
allowAtRules: [],
|
||||
allowFunctions: [],
|
||||
allowMediaConditions: [],
|
||||
allowProperties: [],
|
||||
allowPropertyValues: {},
|
||||
allowSelectors: [],
|
||||
allowUnits: [],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
notBaselineProperty: "Property '{{property}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselinePropertyValue: "Value '{{value}}' of property '{{property}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineAtRule: "At-rule '@{{atRule}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineFunction: "Function '{{function}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineMediaCondition: "Media condition '{{condition}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineSelector: "Selector '{{selector}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineUnit: "Unit '{{unit}}' is not a {{availability}} available baseline feature.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const baselineAvailability = new BaselineAvailability(context.options[0].available);
|
||||
const supportsRules = new SupportsRules();
|
||||
const allowAtRules = new Set(context.options[0].allowAtRules);
|
||||
const allowProperties = new Set(context.options[0].allowProperties);
|
||||
const allowSelectors = new Set(context.options[0].allowSelectors);
|
||||
const allowFunctions = new Set(context.options[0].allowFunctions);
|
||||
const allowMediaConditions = new Set(context.options[0].allowMediaConditions);
|
||||
const allowUnits = new Set(context.options[0].allowUnits);
|
||||
const allowPropertyValuesMap = new Map();
|
||||
for (const [prop, values] of Object.entries(context.options[0].allowPropertyValues)) {
|
||||
allowPropertyValuesMap.set(prop, new Set(values));
|
||||
}
|
||||
/**
|
||||
* Checks a property value identifier to see if it's a baseline feature.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {Identifier} child The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkPropertyValueIdentifier(property, child) {
|
||||
// named colors are always valid
|
||||
if (namedColors.has(child.name)) {
|
||||
return;
|
||||
}
|
||||
const allowedValues = allowPropertyValuesMap.get(property);
|
||||
if (allowedValues?.has(child.name)) {
|
||||
return;
|
||||
}
|
||||
const possiblePropertyValues = propertyValues.get(property);
|
||||
// if we don't know of any possible property values, just skip it
|
||||
if (!possiblePropertyValues) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = possiblePropertyValues.get(child.name);
|
||||
// if we don't know of any possible property values, just skip it
|
||||
if (featureStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: child.loc,
|
||||
messageId: "notBaselinePropertyValue",
|
||||
data: {
|
||||
property,
|
||||
value: child.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks a property value function to see if it's a baseline feature.
|
||||
* @param {FunctionNodePlain} child The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkPropertyValueFunction(child) {
|
||||
if (allowFunctions.has(child.name)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = functions.get(child.name);
|
||||
// if we don't know of any possible property values, just skip it
|
||||
if (featureStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: child.loc,
|
||||
messageId: "notBaselineFunction",
|
||||
data: {
|
||||
function: child.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks a property value unit to see if it's a baseline feature.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {Dimension} child The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkPropertyValueUnit(property, child) {
|
||||
if (allowUnits.has(child.unit)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = units.get(child.unit);
|
||||
// if we don't know of this unit, just skip it
|
||||
if (featureStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: child.loc,
|
||||
messageId: "notBaselineUnit",
|
||||
data: {
|
||||
unit: child.unit,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
"Atrule[name=/^supports$/i]"() {
|
||||
supportsRules.push(new SupportsRule());
|
||||
},
|
||||
"Atrule[name=/^supports$/i] > AtrulePrelude > Condition"(node) {
|
||||
const supportsRule = supportsRules.last();
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const conditionChild = node.children[i];
|
||||
// if a SupportsDeclaration is preceded by "not" then we don't consider it
|
||||
if (conditionChild.type === "Identifier" &&
|
||||
conditionChild.name === "not") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// save the supported properties and values for this at-rule
|
||||
if (conditionChild.type === "SupportsDeclaration") {
|
||||
const { declaration } = conditionChild;
|
||||
const property = declaration.property;
|
||||
const supportedProperty = supportsRule.addProperty(property);
|
||||
declaration.value.children.forEach(child => {
|
||||
if (child.type === "Identifier") {
|
||||
supportedProperty.addIdentifier(child.name);
|
||||
return;
|
||||
}
|
||||
if (child.type === "Dimension") {
|
||||
supportedProperty.addUnit(child.unit);
|
||||
return;
|
||||
}
|
||||
if (child.type === "Function") {
|
||||
supportedProperty.addFunction(child.name);
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (conditionChild.type === "FeatureFunction" &&
|
||||
conditionChild.feature === "selector") {
|
||||
for (const selectorChild of conditionChild.value
|
||||
.children) {
|
||||
supportsRule.addSelector(selectorChild.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rule > Block > Declaration"(node) {
|
||||
const property = node.property;
|
||||
// ignore unknown properties - no-invalid-properties already catches this
|
||||
if (!properties.has(property)) {
|
||||
return;
|
||||
}
|
||||
if (allowProperties.has(property)) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Step 1: Check that the property is in the baseline.
|
||||
*
|
||||
* If the property has been tested in a @supports rule, we don't need to
|
||||
* check it because it won't be applied if the browser doesn't support it.
|
||||
*/
|
||||
if (!supportsRules.hasProperty(property)) {
|
||||
const featureStatus = properties.get(property);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.loc.start,
|
||||
end: {
|
||||
line: node.loc.start.line,
|
||||
column: node.loc.start.column +
|
||||
node.property.length,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineProperty",
|
||||
data: {
|
||||
property,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
/*
|
||||
* If the property isn't in baseline, then we don't go
|
||||
* on to check the values. If the property itself isn't
|
||||
* in baseline then chances are the values aren't too,
|
||||
* and there's no need to report multiple errors for the
|
||||
* same property.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* With tolerant parsing, it's possible that the value is `Raw`
|
||||
* and therefore doesn't have children. If that's the case then
|
||||
* we just exit.
|
||||
*/
|
||||
if (!node.value?.children) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Step 2: Check that the property values are in the baseline.
|
||||
*/
|
||||
for (const child of node.value.children) {
|
||||
if (child.type === "Identifier") {
|
||||
// if the property value has been tested in a @supports rule, don't check it
|
||||
if (!supportsRules.hasPropertyIdentifier(property, child.name)) {
|
||||
checkPropertyValueIdentifier(property, child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (child.type === "Dimension") {
|
||||
if (!supportsRules.hasPropertyUnit(property, child.unit)) {
|
||||
checkPropertyValueUnit(property, child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (child.type === "Function") {
|
||||
if (!supportsRules.hasPropertyFunction(property, child.name)) {
|
||||
checkPropertyValueFunction(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Atrule[name=/^supports$/i]:exit"() {
|
||||
supportsRules.pop();
|
||||
},
|
||||
"Atrule[name=/^media$/i] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"(node) {
|
||||
for (const child of node.children) {
|
||||
// ignore unknown media conditions - no-invalid-at-rules already catches this
|
||||
if (!mediaConditions.has(child.name)) {
|
||||
continue;
|
||||
}
|
||||
if (child.type !== "Feature") {
|
||||
continue;
|
||||
}
|
||||
if (allowMediaConditions.has(child.name)) {
|
||||
continue;
|
||||
}
|
||||
const featureStatus = mediaConditions.get(child.name);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
const loc = child.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: {
|
||||
line: loc.start.line,
|
||||
// add 1 to account for the @ symbol
|
||||
column: loc.start.column + 1,
|
||||
},
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column:
|
||||
// add 1 to account for the @ symbol
|
||||
loc.start.column +
|
||||
child.name.length +
|
||||
1,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineMediaCondition",
|
||||
data: {
|
||||
condition: child.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Atrule(node) {
|
||||
// ignore unknown at-rules - no-invalid-at-rules already catches this
|
||||
const atRuleName = node.name.toLowerCase();
|
||||
if (!atRules.has(atRuleName)) {
|
||||
return;
|
||||
}
|
||||
if (allowAtRules.has(atRuleName)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = atRules.get(atRuleName);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
const loc = node.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
// add 1 to account for the @ symbol
|
||||
column: loc.start.column + node.name.length + 1,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineAtRule",
|
||||
data: {
|
||||
atRule: node.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
"PseudoClassSelector,PseudoElementSelector"(node) {
|
||||
const selector = node.name;
|
||||
if (!selectors.has(selector)) {
|
||||
return;
|
||||
}
|
||||
if (allowSelectors.has(selector)) {
|
||||
return;
|
||||
}
|
||||
// if the selector has been tested in a @supports rule, don't check it
|
||||
if (supportsRules.hasSelector(selector)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = selectors.get(selector);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
const loc = node.loc;
|
||||
// some selectors are prefixed with the : or :: symbols
|
||||
let prefixSymbolLength = 0;
|
||||
if (node.type.startsWith("PseudoClass")) {
|
||||
prefixSymbolLength = 1;
|
||||
}
|
||||
else if (node.type.startsWith("PseudoElement")) {
|
||||
prefixSymbolLength = 2;
|
||||
}
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column +
|
||||
selector.length +
|
||||
prefixSymbolLength,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineSelector",
|
||||
data: {
|
||||
selector,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
NestingSelector(node) {
|
||||
// NestingSelector implies CSS nesting
|
||||
const selector = "nesting";
|
||||
if (allowSelectors.has(selector)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = selectors.get(selector);
|
||||
if (baselineAvailability.isSupported(featureStatus)) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "notBaselineSelector",
|
||||
data: {
|
||||
selector,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowUnnamedLayers: {
|
||||
type: "boolean";
|
||||
};
|
||||
requireImportLayers: {
|
||||
type: "boolean";
|
||||
};
|
||||
layerNamePattern: {
|
||||
type: "string";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowUnnamedLayers: false;
|
||||
requireImportLayers: true;
|
||||
layerNamePattern: string;
|
||||
}];
|
||||
namespace messages {
|
||||
let missingLayer: string;
|
||||
let missingLayerName: string;
|
||||
let missingImportLayer: string;
|
||||
let layerNameMismatch: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: UseLayersOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: UseLayersMessageIds;
|
||||
}>): {
|
||||
"Atrule[name=/^import$/i]"(node: any): void;
|
||||
Layer(node: import("@eslint/css-tree").Layer): void;
|
||||
"Atrule[name=/^layer$/i]"(node: any): void;
|
||||
"Atrule[name=/^layer$/i]:exit"(): void;
|
||||
Rule(node: import("@eslint/css-tree").RulePlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type UseLayersMessageIds = "missingLayer" | "missingLayerName" | "missingImportLayer" | "layerNameMismatch";
|
||||
export type UseLayersOptions = [{
|
||||
allowUnnamedLayers?: boolean;
|
||||
requireImportLayers?: boolean;
|
||||
layerNamePattern?: string;
|
||||
}];
|
||||
export type UseLayersRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: UseLayersOptions;
|
||||
MessageIds: UseLayersMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @fileoverview Rule to require layers in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"missingLayer" | "missingLayerName" | "missingImportLayer" | "layerNameMismatch"} UseLayersMessageIds
|
||||
* @typedef {[{
|
||||
* allowUnnamedLayers?: boolean,
|
||||
* requireImportLayers?: boolean,
|
||||
* layerNamePattern?: string
|
||||
* }]} UseLayersOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: UseLayersOptions, MessageIds: UseLayersMessageIds }>} UseLayersRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {UseLayersRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Require use of layers",
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/use-layers.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowUnnamedLayers: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireImportLayers: {
|
||||
type: "boolean",
|
||||
},
|
||||
layerNamePattern: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowUnnamedLayers: false,
|
||||
requireImportLayers: true,
|
||||
layerNamePattern: "",
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
missingLayer: "Expected rule to be within a layer.",
|
||||
missingLayerName: "Expected layer to have a name.",
|
||||
missingImportLayer: "Expected import to be within a layer.",
|
||||
layerNameMismatch: "Expected layer name '{{ name }}' to match pattern '{{pattern}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let layerDepth = 0;
|
||||
const options = context.options[0];
|
||||
const layerNameRegex = options.layerNamePattern
|
||||
? new RegExp(options.layerNamePattern, "u")
|
||||
: null;
|
||||
return {
|
||||
"Atrule[name=/^import$/i]"(node) {
|
||||
// layer, if present, must always be the second child of the prelude
|
||||
const secondChild = node.prelude?.children[1];
|
||||
const layerNode = secondChild?.name === "layer" ? secondChild : null;
|
||||
if (options.requireImportLayers && !layerNode) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "missingImportLayer",
|
||||
});
|
||||
}
|
||||
if (layerNode) {
|
||||
const isLayerFunction = layerNode.type === "Function";
|
||||
if (!options.allowUnnamedLayers && !isLayerFunction) {
|
||||
context.report({
|
||||
loc: layerNode.loc,
|
||||
messageId: "missingLayerName",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Layer(node) {
|
||||
if (!layerNameRegex) {
|
||||
return;
|
||||
}
|
||||
const parts = node.name.split(".");
|
||||
let currentPos = 0;
|
||||
parts.forEach((part, index) => {
|
||||
if (!layerNameRegex.test(part)) {
|
||||
const startColumn = node.loc.start.column + currentPos;
|
||||
const endColumn = startColumn + part.length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: {
|
||||
line: node.loc.start.line,
|
||||
column: startColumn,
|
||||
},
|
||||
end: {
|
||||
line: node.loc.start.line,
|
||||
column: endColumn,
|
||||
},
|
||||
},
|
||||
messageId: "layerNameMismatch",
|
||||
data: {
|
||||
name: part,
|
||||
pattern: options.layerNamePattern,
|
||||
},
|
||||
});
|
||||
}
|
||||
currentPos += part.length;
|
||||
// add 1 to account for the . symbol
|
||||
if (index < parts.length - 1) {
|
||||
currentPos += 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
"Atrule[name=/^layer$/i]"(node) {
|
||||
layerDepth++;
|
||||
if (!options.allowUnnamedLayers && !node.prelude) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "missingLayerName",
|
||||
});
|
||||
}
|
||||
},
|
||||
"Atrule[name=/^layer$/i]:exit"() {
|
||||
layerDepth--;
|
||||
},
|
||||
Rule(node) {
|
||||
if (layerDepth > 0) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "missingLayer",
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @fileoverview Additional types for this package.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
import type { RuleVisitor } from "@eslint/core";
|
||||
import type { CssNodePlain, StyleSheetPlain } from "@eslint/css-tree";
|
||||
import type { CustomRuleDefinitionType, CustomRuleTypeDefinitions, CustomRuleVisitorWithExit } from "@eslint/plugin-kit";
|
||||
import type { CSSLanguageOptions, CSSSourceCode } from "./index.js";
|
||||
/**
|
||||
* A CSS syntax element, including nodes and comments.
|
||||
*/
|
||||
export type CSSSyntaxElement = CssNodePlain;
|
||||
type CSSNodeVisitor = {
|
||||
[Node in CssNodePlain as Node["type"]]: Node extends StyleSheetPlain ? ((node: Node) => void) | undefined : ((node: Node, parent: CssNodePlain) => void) | undefined;
|
||||
};
|
||||
/**
|
||||
* A visitor for CSS nodes.
|
||||
*/
|
||||
export interface CSSRuleVisitor extends RuleVisitor, Partial<CustomRuleVisitorWithExit<CSSNodeVisitor>> {
|
||||
}
|
||||
export type CSSRuleDefinitionTypeOptions = CustomRuleTypeDefinitions;
|
||||
/**
|
||||
* A rule definition for CSS.
|
||||
*/
|
||||
export type CSSRuleDefinition<Options extends Partial<CSSRuleDefinitionTypeOptions> = {}> = CustomRuleDefinitionType<{
|
||||
LangOptions: CSSLanguageOptions;
|
||||
Code: CSSSourceCode;
|
||||
Visitor: CSSRuleVisitor;
|
||||
Node: CSSSyntaxElement;
|
||||
}, Options>;
|
||||
export {};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @fileoverview Additional types for this package.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
export {};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for ESLint CSS plugin.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/**
|
||||
* @import { SyntaxMatchError, SyntaxReferenceError } from "@eslint/css-tree"
|
||||
*/
|
||||
/**
|
||||
* Determines if an error is a syntax match error.
|
||||
* @param {Object} error The error object to check.
|
||||
* @returns {error is SyntaxMatchError} True if the error is a syntax match error, false if not.
|
||||
*/
|
||||
export function isSyntaxMatchError(error: any): error is SyntaxMatchError;
|
||||
/**
|
||||
* Determines if an error is a syntax reference error.
|
||||
* @param {Object} error The error object to check.
|
||||
* @returns {error is SyntaxReferenceError} True if the error is a syntax reference error, false if not.
|
||||
*/
|
||||
export function isSyntaxReferenceError(error: any): error is SyntaxReferenceError;
|
||||
import type { SyntaxMatchError } from "@eslint/css-tree";
|
||||
import type { SyntaxReferenceError } from "@eslint/css-tree";
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @fileoverview Utility functions for ESLint CSS plugin.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { SyntaxMatchError, SyntaxReferenceError } from "@eslint/css-tree"
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Determines if an error is a syntax match error.
|
||||
* @param {Object} error The error object to check.
|
||||
* @returns {error is SyntaxMatchError} True if the error is a syntax match error, false if not.
|
||||
*/
|
||||
export function isSyntaxMatchError(error) {
|
||||
return typeof error.syntax === "string";
|
||||
}
|
||||
/**
|
||||
* Determines if an error is a syntax reference error.
|
||||
* @param {Object} error The error object to check.
|
||||
* @returns {error is SyntaxReferenceError} True if the error is a syntax reference error, false if not.
|
||||
*/
|
||||
export function isSyntaxReferenceError(error) {
|
||||
return typeof error.reference === "string";
|
||||
}
|
||||
Reference in New Issue
Block a user