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