first commit

This commit is contained in:
2026-08-06 12:00:39 +10:00
commit 91221f5a8e
3259 changed files with 569069 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+333
View File
@@ -0,0 +1,333 @@
# ESLint CSS Language Plugin
## Overview
This package contains a plugin that allows you to natively lint CSS files using ESLint.
**Important:** This plugin requires ESLint v9.15.0 or higher and you must be using the [new configuration system](https://eslint.org/docs/latest/use/configure/configuration-files).
## Prerequisites
In order to use the ESLint CSS plugin, you must have the following installed:
1. [Node.js](https://nodejs.org)
1. [ESLint](https://eslint.org/docs/latest/use/getting-started)
## Installation
For Node.js and compatible runtimes:
```shell
npm install @eslint/css -D
# or
yarn add @eslint/css -D
# or
pnpm install @eslint/css -D
# or
bun add @eslint/css -D
```
For Deno (experimental):
```shell
deno add jsr:@eslint/css
```
### Configurations
| **Configuration Name** | **Description** |
| ---------------------- | ------------------------------ |
| `recommended` | Enables all recommended rules. |
In your `eslint.config.js` file, import `@eslint/css` and include the recommended config:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
export default defineConfig([
// lint CSS files
{
files: ["**/*.css"],
language: "css/css",
plugins: { css },
extends: ["css/recommended"],
},
// your other configs here
]);
```
### Rules
<!-- NOTE: The following table is autogenerated. Do not manually edit. -->
<!-- Rule Table Start -->
| **Rule Name** | **Description** | **Recommended** |
| :----------------------------------------------------------------------------------- | :---------------------------------------------------- | :-------------: |
| [`font-family-fallbacks`](./docs/rules/font-family-fallbacks.md) | Enforce use of fallback fonts and a generic font last | yes |
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
| [`no-duplicate-keyframe-selectors`](./docs/rules/no-duplicate-keyframe-selectors.md) | Disallow duplicate selectors within keyframe blocks | yes |
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
| [`no-invalid-at-rule-placement`](./docs/rules/no-invalid-at-rule-placement.md) | Disallow invalid placement of at-rules | yes |
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
| [`no-unmatchable-selectors`](./docs/rules/no-unmatchable-selectors.md) | Disallow unmatchable selectors | yes |
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
| [`selector-complexity`](./docs/rules/selector-complexity.md) | Disallow and limit CSS selectors | no |
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
<!-- Rule Table End -->
**Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose.
In order to individually configure a rule in your `eslint.config.js` file, import `@eslint/css` and configure each rule with a prefix:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
export default defineConfig([
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
rules: {
"css/no-empty-blocks": "error",
},
},
]);
```
You can individually configure, disable, and enable rules in CSS using comments, such as:
<!-- prettier-ignore -->
```css
/* eslint css/no-empty-blocks: error */
/* eslint-disable css/no-empty-blocks -- this one is ok */
a {
}
/* eslint-enable css/no-empty-blocks */
b { /* eslint-disable-line css/no-empty-blocks */
}
/* eslint-disable-next-line css/no-empty-blocks */
em {
}
```
### Languages
| **Language Name** | **Description** |
| ----------------- | ---------------------- |
| `css` | Parse CSS stylesheets. |
In order to individually configure a language in your `eslint.config.js` file, import `@eslint/css` and configure a `language`:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
export default defineConfig([
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
rules: {
"css/no-empty-blocks": "error",
},
},
]);
```
#### Tolerant Mode
By default, the CSS parser runs in strict mode, which reports all parsing errors. If you'd like to allow recoverable parsing errors (those that the browser automatically fixes on its own), you can set the `tolerant` option to `true`:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
export default defineConfig([
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
languageOptions: {
tolerant: true,
},
rules: {
"css/no-empty-blocks": "error",
},
},
]);
```
Setting `tolerant` to `true` is necessary if you are using custom syntax, such as [PostCSS](https://postcss.org/) plugins, that aren't part of the standard CSS syntax.
#### Configuring Custom Syntax
The CSS lexer comes prebuilt with a set of known syntax for CSS that is used in rules like `no-invalid-properties` to validate CSS code. While this works for most cases, there may be cases when you want to define your own extensions to CSS, and this can be done using the `customSyntax` language option.
The `customSyntax` option accepts either an object or a function:
**Object-based syntax**: An object that uses the [CSSTree format](https://github.com/csstree/csstree/blob/master/data/patch.json) for defining custom syntax, which allows you to specify at-rules, properties, and some types. For example, suppose you'd like to define a custom at-rule that looks like this:
```css
@my-at-rule "hello world!";
```
You can configure that syntax as follows:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
export default defineConfig([
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
languageOptions: {
customSyntax: {
atrules: {
"my-at-rule": {
prelude: "<string>",
},
},
},
},
rules: {
"css/no-empty-blocks": "error",
},
},
]);
```
**Function-based syntax**: A function that receives the default CSS syntax data and returns a custom syntax configuration. This is useful when you want to extend the base syntax rather than replace it. For example:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
export default defineConfig([
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
languageOptions: {
customSyntax: defaultSyntax => ({
...defaultSyntax,
properties: {
...defaultSyntax.properties,
"-webkit-custom": "<length>",
"-moz-custom": "<color>",
},
}),
},
rules: {
"css/no-empty-blocks": "error",
},
},
]);
```
#### Configuring Tailwind Syntax
[Tailwind](https://tailwindcss.com) specifies some extensions to CSS that will otherwise be flagged as invalid by the rules in this plugin. To properly parse Tailwind-specific syntax, install the [`tailwind-csstree`](https://npmjs.com/package/tailwind-csstree) package:
```shell
npm i tailwind-csstree --save-dev
```
Then include it in your configuration using `languageOptions.customSyntax`:
```js
// eslint.config.js
import { defineConfig } from "eslint/config";
import css from "@eslint/css";
import { tailwind4 } from "tailwind-csstree";
export default defineConfig([
{
files: ["**/*.css"],
plugins: {
css,
},
language: "css/css",
languageOptions: {
customSyntax: tailwind4,
},
rules: {
"css/no-empty-blocks": "error",
},
},
]);
```
## Editor and IDE Setup
### Visual Studio Code
First, ensure you have the [ESLint plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) installed.
Then, edit `eslint.validate` in your `settings.json` file to include `css`:
```json
{
"eslint.validate": ["css"]
}
```
### JetBrains WebStorm
For any [JetBrains WebStorm](https://www.jetbrains.com/webstorm/), configure the [ESLint scope](https://www.jetbrains.com/help/webstorm/eslint.html#ws_eslint_configure_scope) to include `css`, such as:
```text
**/*.{js,ts,jsx,tsx,cjs,cts,mjs,mts,html,vue,css}
```
## License
Apache 2.0
<!-- NOTE: This section is autogenerated. Do not manually edit.-->
<!--sponsorsstart-->
## Sponsors
The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate)
to get your logo on our READMEs and [website](https://eslint.org/sponsors).
<h3>Platinum Sponsors</h3>
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a></p><h3>Gold Sponsors</h3>
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a> <a href="https://www.coderabbit.ai/?utm_source=cr_org&utm_medium=github"><img src="https://avatars.githubusercontent.com/u/132028505" alt="CodeRabbit" height="96"></a></p><h3>Silver Sponsors</h3>
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/d472863/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/2d6c3b6/logo.png" alt="Liftoff" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://opensource.sap.com"><img src="https://avatars.githubusercontent.com/u/2531208" alt="SAP" height="32"></a> <a href="https://www.crawljobs.com/"><img src="https://images.opencollective.com/crawljobs-poland/fa43a17/logo.png" alt="CrawlJobs" height="32"></a> <a href="https://depot.dev"><img src="https://images.opencollective.com/depot/39125a1/logo.png" alt="Depot" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://citadel.co.jp"><img src="https://avatars.githubusercontent.com/u/75781367" alt="Citadel AI" height="32"></a></p>
<h3>Technology Sponsors</h3>
Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
<!--sponsorsend-->
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>>;
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
/**
* @fileoverview Color information for CSS.
* @author Nicholas C. Zakas
*/
export const namedColors: Set<string>;
+154
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
});
}
}
@@ -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
View File
@@ -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;
}
}
@@ -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[];
}
@@ -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: [],
};
@@ -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";
@@ -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);
}
}
}
},
};
},
});
@@ -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
View File
@@ -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);
}
},
};
},
});
@@ -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";
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
]);
},
},
],
});
}
},
};
},
});
@@ -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";
@@ -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;
},
};
},
});
@@ -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
View File
@@ -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,
},
});
}
},
};
},
});
@@ -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";
@@ -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,
},
});
});
}
},
};
},
});
@@ -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";
@@ -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,
},
});
}
}
},
};
},
});
@@ -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";
@@ -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) },
});
}
},
};
},
});
@@ -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";
@@ -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);
},
},
],
});
}
},
};
},
});
@@ -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
View File
@@ -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(", "),
},
});
}
}
}
},
};
},
});
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,5 @@
/**
* @fileoverview Additional types for this package.
* @author Nicholas C. Zakas
*/
export {};
+21
View File
@@ -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
View File
@@ -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";
}
+103
View File
@@ -0,0 +1,103 @@
{
"name": "@eslint/css",
"version": "1.4.0",
"description": "CSS linting plugin for ESLint",
"author": "Nicholas C. Zakas",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write"
],
"!(*.js)": "prettier --write --ignore-unknown",
"{src/rules/*.js,tools/update-rules-docs.js,README.md}": [
"npm run build:update-rules-docs",
"git add README.md"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/eslint/css.git"
},
"bugs": {
"url": "https://github.com/eslint/css/issues"
},
"homepage": "https://github.com/eslint/css#readme",
"scripts": {
"build": "npm run build:rules && npm run build:types && npm run build:update-rules-docs",
"build:baseline": "node tools/generate-baseline.js",
"build:rules": "node tools/build-rules.js",
"build:types": "tsc",
"build:update-rules-docs": "node tools/update-rules-docs.js",
"prepare": "npm run build",
"pretest": "npm run build",
"lint": "eslint",
"lint:fix": "eslint --fix",
"lint:types": "attw --pack --profile esm-only",
"lint:unused": "knip",
"fmt": "prettier --write .",
"fmt:check": "prettier --check .",
"test": "mocha \"tests/**/*.test.js\"",
"test:coverage": "c8 npm test",
"test:jsr": "npx -y jsr@latest publish --dry-run",
"test:types": "npm run build && tsc -p tests/types/tsconfig.json",
"test:types:5.3": "npx -p typescript@5.3 -y -- tsc -p tests/types/tsconfig.legacy.json",
"test:types:5.x": "npx -p typescript@5.x -y -- tsc -p tests/types/tsconfig.json",
"test:types:7.x": "npx -p @typescript/native-preview@latest -y -- tsgo -p tests/types/tsconfig.json",
"test:types:all": "npm run test:types && npm run test:types:5.3 && npm run test:types:5.x && npm run test:types:7.x"
},
"keywords": [
"eslint",
"eslint-plugin",
"eslintplugin",
"css",
"linting"
],
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1",
"@eslint/css-tree": "^4.0.4",
"@eslint/plugin-kit": "^0.7.2"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.3",
"@eslint/json": "^2.0.0",
"@types/node": "^20.19.0",
"@webref/css": "^8.6.0",
"c8": "^11.0.0",
"dedent": "^1.5.3",
"eslint": "^10.0.0",
"eslint-config-eslint": "^14.0.0",
"eslint-plugin-eslint-plugin": "^7.3.2",
"globals": "^17.0.0",
"knip": "^6.0.0",
"lint-staged": "^16.0.0",
"mdast-util-from-markdown": "^2.0.2",
"mocha": "^11.0.0",
"prettier": "3.8.4",
"tailwind-csstree": "^0.3.0",
"typescript": "^6.0.3",
"web-features": "^3.32.0",
"yorkie": "^2.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
}