Skip to content

Commit d08df74

Browse files
nzakasmdjermanovic
andauthored
fix: Catch more parse errors (#97)
* fix: Catch more parse errors * Update src/languages/css-language.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 4184812 commit d08df74

File tree

3 files changed

+192
-5
lines changed

3 files changed

+192
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"license": "Apache-2.0",
8787
"dependencies": {
8888
"@eslint/core": "^0.10.0",
89-
"@eslint/css-tree": "^3.3.0",
89+
"@eslint/css-tree": "^3.3.1",
9090
"@eslint/plugin-kit": "^0.2.5"
9191
},
9292
"devDependencies": {

src/languages/css-language.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
lexer as originalLexer,
1313
fork,
1414
toPlainObject,
15+
tokenTypes,
1516
} from "@eslint/css-tree";
1617
import { CSSSourceCode } from "./css-source-code.js";
1718
import { visitorKeys } from "./css-visitor-keys.js";
@@ -38,6 +39,23 @@ import { visitorKeys } from "./css-visitor-keys.js";
3839
* @property {SyntaxConfig} [customSyntax] Custom syntax to use for parsing.
3940
*/
4041

42+
//-----------------------------------------------------------------------------
43+
// Helpers
44+
//-----------------------------------------------------------------------------
45+
46+
const blockOpenerTokenTypes = new Map([
47+
[tokenTypes.Function, ")"],
48+
[tokenTypes.LeftCurlyBracket, "}"],
49+
[tokenTypes.LeftParenthesis, ")"],
50+
[tokenTypes.LeftSquareBracket, "]"],
51+
]);
52+
53+
const blockCloserTokenTypes = new Map([
54+
[tokenTypes.RightCurlyBracket, "{"],
55+
[tokenTypes.RightParenthesis, "("],
56+
[tokenTypes.RightSquareBracket, "["],
57+
]);
58+
4159
//-----------------------------------------------------------------------------
4260
// Exports
4361
//-----------------------------------------------------------------------------
@@ -154,10 +172,64 @@ export class CSSLanguage {
154172
},
155173
onParseError(error) {
156174
if (!tolerant) {
157-
// @ts-ignore -- types are incorrect
158175
errors.push(error);
159176
}
160177
},
178+
onToken(type, start, end, index) {
179+
if (tolerant) {
180+
return;
181+
}
182+
183+
switch (type) {
184+
// these already generate errors
185+
case tokenTypes.BadString:
186+
case tokenTypes.BadUrl:
187+
break;
188+
189+
default:
190+
/* eslint-disable new-cap -- This is a valid call */
191+
if (this.isBlockOpenerTokenType(type)) {
192+
if (
193+
this.getBlockTokenPairIndex(index) ===
194+
-1
195+
) {
196+
const loc = this.getRangeLocation(
197+
start,
198+
end,
199+
);
200+
errors.push(
201+
parse.SyntaxError(
202+
`Missing closing ${blockOpenerTokenTypes.get(type)}`,
203+
text,
204+
start,
205+
loc.start.line,
206+
loc.start.column,
207+
),
208+
);
209+
}
210+
} else if (this.isBlockCloserTokenType(type)) {
211+
if (
212+
this.getBlockTokenPairIndex(index) ===
213+
-1
214+
) {
215+
const loc = this.getRangeLocation(
216+
start,
217+
end,
218+
);
219+
errors.push(
220+
parse.SyntaxError(
221+
`Missing opening ${blockCloserTokenTypes.get(type)}`,
222+
text,
223+
start,
224+
loc.start.line,
225+
loc.start.column,
226+
),
227+
);
228+
}
229+
}
230+
/* eslint-enable new-cap -- This is a valid call */
231+
}
232+
},
161233
}),
162234
);
163235

tests/languages/css-language.test.js

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ describe("CSSLanguage", () => {
103103
}, /Expected an object value for 'customSyntax' option/u);
104104
});
105105

106-
// https://github.com/csstree/csstree/issues/301
107-
it.skip("should return an error when EOF is discovered before block close", () => {
106+
it("should return an error when EOF is discovered before block close", () => {
108107
const language = new CSSLanguage();
109108
const result = language.parse({
110109
body: "a {",
@@ -114,7 +113,123 @@ describe("CSSLanguage", () => {
114113
assert.strictEqual(result.ok, false);
115114
assert.strictEqual(result.ast, undefined);
116115
assert.strictEqual(result.errors.length, 1);
117-
assert.strictEqual(result.errors[0].message, "Colon is expected");
116+
assert.strictEqual(result.errors[0].message, "Missing closing }");
117+
assert.strictEqual(result.errors[0].line, 1);
118+
assert.strictEqual(result.errors[0].column, 3);
119+
assert.strictEqual(result.errors[0].offset, 2);
120+
});
121+
122+
it("should return an error when a CSS bad string is found", () => {
123+
const language = new CSSLanguage();
124+
const result = language.parse({
125+
body: "a { content: 'this\nstring is not properly closed' }",
126+
path: "test.css",
127+
});
128+
129+
assert.strictEqual(result.ok, false);
130+
assert.strictEqual(result.ast, undefined);
131+
assert.strictEqual(result.errors.length, 2);
132+
assert.strictEqual(result.errors[0].message, "Missing closing }");
133+
assert.strictEqual(result.errors[0].line, 1);
134+
assert.strictEqual(result.errors[0].column, 3);
135+
assert.strictEqual(result.errors[0].offset, 2);
136+
assert.strictEqual(result.errors[1].message, "Unexpected input");
137+
assert.strictEqual(result.errors[1].line, 1);
138+
assert.strictEqual(result.errors[1].column, 14);
139+
assert.strictEqual(result.errors[1].offset, 13);
140+
});
141+
142+
it("should return an error when a CSS bad URL is found", () => {
143+
const language = new CSSLanguage();
144+
const result = language.parse({
145+
body: "a { background: url(foo bar.png) }",
146+
path: "test.css",
147+
});
148+
149+
assert.strictEqual(result.ok, false);
150+
assert.strictEqual(result.ast, undefined);
151+
assert.strictEqual(result.errors.length, 1);
152+
assert.strictEqual(result.errors[0].message, "Unexpected input");
153+
assert.strictEqual(result.errors[0].line, 1);
154+
assert.strictEqual(result.errors[0].column, 17);
155+
assert.strictEqual(result.errors[0].offset, 16);
156+
});
157+
158+
it("should return an error when braces are unclosed", () => {
159+
const language = new CSSLanguage();
160+
const result = language.parse({
161+
body: "a { color: red;",
162+
path: "test.css",
163+
});
164+
165+
assert.strictEqual(result.ok, false);
166+
assert.strictEqual(result.ast, undefined);
167+
assert.strictEqual(result.errors.length, 1);
168+
assert.strictEqual(result.errors[0].message, "Missing closing }");
169+
assert.strictEqual(result.errors[0].line, 1);
170+
assert.strictEqual(result.errors[0].column, 3);
171+
assert.strictEqual(result.errors[0].offset, 2);
172+
});
173+
174+
it("should return an error when square brackets are unclosed", () => {
175+
const language = new CSSLanguage();
176+
const result = language.parse({
177+
body: "a[foo { color: red; }",
178+
path: "test.css",
179+
});
180+
181+
assert.strictEqual(result.ok, false);
182+
assert.strictEqual(result.ast, undefined);
183+
assert.strictEqual(result.errors.length, 3); // other errors caused by the first one
184+
assert.strictEqual(result.errors[0].message, "Missing closing ]");
185+
assert.strictEqual(result.errors[0].line, 1);
186+
assert.strictEqual(result.errors[0].column, 2);
187+
assert.strictEqual(result.errors[0].offset, 1);
188+
});
189+
190+
it("should return an error when parentheses are unclosed", () => {
191+
const language = new CSSLanguage();
192+
const result = language.parse({
193+
body: "@supports (color: red {}",
194+
path: "test.css",
195+
});
196+
197+
assert.strictEqual(result.ok, false);
198+
assert.strictEqual(result.ast, undefined);
199+
assert.strictEqual(result.errors.length, 2); // other errors caused by the first one
200+
assert.strictEqual(result.errors[0].message, "Missing closing )");
201+
assert.strictEqual(result.errors[0].line, 1);
202+
assert.strictEqual(result.errors[0].column, 11);
203+
assert.strictEqual(result.errors[0].offset, 10);
204+
});
205+
206+
it("should return an error when function parentheses is unclosed", () => {
207+
const language = new CSSLanguage();
208+
const result = language.parse({
209+
body: "a { width: min(40%, 400px; }",
210+
path: "test.css",
211+
});
212+
213+
assert.strictEqual(result.ok, false);
214+
assert.strictEqual(result.ast, undefined);
215+
assert.strictEqual(result.errors.length, 4); // other errors caused by the first one
216+
assert.strictEqual(result.errors[1].message, "Missing closing )");
217+
assert.strictEqual(result.errors[1].line, 1);
218+
assert.strictEqual(result.errors[1].column, 12);
219+
assert.strictEqual(result.errors[1].offset, 11);
220+
});
221+
222+
it("should not return an error when braces are unclosed and tolerant: true is used", () => {
223+
const language = new CSSLanguage();
224+
const result = language.parse(
225+
{
226+
body: "a { color: red;",
227+
path: "test.css",
228+
},
229+
{ languageOptions: { tolerant: true } },
230+
);
231+
232+
assert.strictEqual(result.ok, true);
118233
});
119234
});
120235

0 commit comments

Comments
 (0)