Skip to content

Commit 9e4b2dd

Browse files
authored
feat: Add tolerant parsing mode (#38)
fixes #29
1 parent 82d07c2 commit 9e4b2dd

File tree

5 files changed

+138
-8
lines changed

5 files changed

+138
-8
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,31 @@ export default [
132132
];
133133
```
134134

135+
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`:
136+
137+
```js
138+
// eslint.config.js
139+
import css from "@eslint/css";
140+
141+
export default [
142+
{
143+
files: ["**/*.css"],
144+
plugins: {
145+
css,
146+
},
147+
language: "css/css",
148+
languageOptions: {
149+
tolerant: true,
150+
},
151+
rules: {
152+
"css/no-empty-blocks": "error",
153+
},
154+
},
155+
];
156+
```
157+
158+
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.
159+
135160
## License
136161

137162
Apache 2.0

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"css-tree": "^3.0.1"
7474
},
7575
"devDependencies": {
76-
"@eslint/core": "^0.6.0",
76+
"@eslint/core": "^0.7.0",
7777
"@eslint/json": "^0.5.0",
7878
"@types/eslint": "^8.56.10",
7979
"c8": "^9.1.0",

src/languages/css-language.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import { visitorKeys } from "./css-visitor-keys.js";
2525
/** @typedef {import("@eslint/core").File} File */
2626
/** @typedef {import("@eslint/core").FileError} FileError */
2727

28+
/**
29+
* @typedef {Object} CSSLanguageOptions
30+
* @property {boolean} [tolerant] Whether to be tolerant of recoverable parsing errors.
31+
*/
32+
2833
//-----------------------------------------------------------------------------
2934
// Exports
3035
//-----------------------------------------------------------------------------
@@ -64,21 +69,38 @@ export class CSSLanguage {
6469
*/
6570
visitorKeys = visitorKeys;
6671

72+
/**
73+
* The default language options.
74+
* @type {CSSLanguageOptions}
75+
*/
76+
defaultLanguageOptions = {
77+
tolerant: false,
78+
};
79+
6780
/**
6881
* Validates the language options.
69-
* @returns {void}
82+
* @param {CSSLanguageOptions} languageOptions The language options to validate.
7083
* @throws {Error} When the language options are invalid.
7184
*/
72-
validateLanguageOptions() {
73-
// noop
85+
validateLanguageOptions(languageOptions) {
86+
if (
87+
"tolerant" in languageOptions &&
88+
typeof languageOptions.tolerant !== "boolean"
89+
) {
90+
throw new TypeError(
91+
"Expected a boolean value for 'tolerant' option.",
92+
);
93+
}
7494
}
7595

7696
/**
7797
* Parses the given file into an AST.
7898
* @param {File} file The virtual file to parse.
99+
* @param {Object} [context] The parsing context.
100+
* @param {CSSLanguageOptions} [context.languageOptions] The language options to use for parsing.
79101
* @returns {ParseResult} The result of parsing.
80102
*/
81-
parse(file) {
103+
parse(file, { languageOptions = {} } = {}) {
82104
// Note: BOM already removed
83105
const text = /** @type {string} */ (file.body);
84106

@@ -88,6 +110,8 @@ export class CSSLanguage {
88110
/** @type {FileError[]} */
89111
const errors = [];
90112

113+
const { tolerant } = languageOptions;
114+
91115
/*
92116
* Check for parsing errors first. If there's a parsing error, nothing
93117
* else can happen. However, a parsing error does not throw an error
@@ -107,8 +131,10 @@ export class CSSLanguage {
107131
});
108132
},
109133
onParseError(error) {
110-
// @ts-ignore -- types are incorrect
111-
errors.push(error);
134+
if (!tolerant) {
135+
// @ts-ignore -- types are incorrect
136+
errors.push(error);
137+
}
112138
},
113139
}),
114140
);

tests/languages/css-language.test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe("CSSLanguage", () => {
3838
assert.strictEqual(result.ast.children[0].type, "Rule");
3939
});
4040

41-
it("should return an error when parsing invalid CSS", () => {
41+
it("should return an error when CSS has a recoverable error", () => {
4242
const language = new CSSLanguage();
4343
const result = language.parse({
4444
body: "a { foo; bar: 1! }",
@@ -61,6 +61,19 @@ describe("CSSLanguage", () => {
6161
assert.strictEqual(result.errors[1].column, 18);
6262
});
6363

64+
it("should not return an error when CSS has a recoverable error and tolerant: true is used", () => {
65+
const language = new CSSLanguage();
66+
const result = language.parse(
67+
{
68+
body: "a { foo; bar: 1! }",
69+
path: "test.css",
70+
},
71+
{ languageOptions: { tolerant: true } },
72+
);
73+
74+
assert.strictEqual(result.ok, true);
75+
});
76+
6477
// https://github.com/csstree/csstree/issues/301
6578
it.skip("should return an error when EOF is discovered before block close", () => {
6679
const language = new CSSLanguage();

tests/plugin/eslint.test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,72 @@ describe("Plugin", () => {
3737
});
3838
});
3939

40+
describe("languageOptions", () => {
41+
const config = {
42+
files: ["*.css"],
43+
plugins: {
44+
css,
45+
},
46+
language: "css/css",
47+
rules: {
48+
"css/no-empty-blocks": "error",
49+
},
50+
};
51+
52+
describe("tolerant", () => {
53+
it("should not report a parsing error when CSS has a recoverable error and tolerant: true is used", async () => {
54+
const code = "a { foo; bar: 1! }";
55+
56+
const eslint = new ESLint({
57+
overrideConfigFile: true,
58+
overrideConfig: {
59+
...config,
60+
languageOptions: {
61+
tolerant: true,
62+
},
63+
},
64+
});
65+
66+
const results = await eslint.lintText(code, {
67+
filePath: "test.css",
68+
});
69+
70+
assert.strictEqual(results.length, 1);
71+
assert.strictEqual(results[0].messages.length, 0);
72+
});
73+
74+
it("should report a parsing error when CSS has a recoverable error and tolerant is undefined", async () => {
75+
const code = "a { foo; bar: 1! }";
76+
77+
const eslint = new ESLint({
78+
overrideConfigFile: true,
79+
overrideConfig: config,
80+
});
81+
82+
const results = await eslint.lintText(code, {
83+
filePath: "test.css",
84+
});
85+
86+
assert.strictEqual(results.length, 1);
87+
assert.strictEqual(results[0].messages.length, 2);
88+
89+
assert.strictEqual(
90+
results[0].messages[0].message,
91+
"Parsing error: Colon is expected",
92+
);
93+
assert.strictEqual(results[0].messages[0].line, 1);
94+
assert.strictEqual(results[0].messages[0].column, 8);
95+
96+
assert.strictEqual(
97+
results[0].messages[1].message,
98+
"Parsing error: Identifier is expected",
99+
);
100+
assert.strictEqual(results[0].messages[1].line, 1);
101+
assert.strictEqual(results[0].messages[1].column, 18);
102+
});
103+
});
104+
});
105+
40106
describe("Configuration Comments", () => {
41107
const config = {
42108
files: ["*.css"],

0 commit comments

Comments
 (0)