Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.10.0",
"@eslint/css-tree": "^3.3.0",
"@eslint/css-tree": "^3.3.1",
"@eslint/plugin-kit": "^0.2.5"
},
"devDependencies": {
Expand Down
74 changes: 73 additions & 1 deletion src/languages/css-language.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
lexer as originalLexer,
fork,
toPlainObject,
tokenTypes,
} from "@eslint/css-tree";
import { CSSSourceCode } from "./css-source-code.js";
import { visitorKeys } from "./css-visitor-keys.js";
Expand All @@ -38,6 +39,23 @@ import { visitorKeys } from "./css-visitor-keys.js";
* @property {SyntaxConfig} [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, "["],
]);

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -154,10 +172,64 @@ export class CSSLanguage {
},
onParseError(error) {
if (!tolerant) {
// @ts-ignore -- types are incorrect
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 */
}
},
}),
);

Expand Down
121 changes: 118 additions & 3 deletions tests/languages/css-language.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ describe("CSSLanguage", () => {
}, /Expected an object value for 'customSyntax' option/u);
});

// https://github.com/csstree/csstree/issues/301
it.skip("should return an error when EOF is discovered before block close", () => {
it("should return an error when EOF is discovered before block close", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "a {",
Expand All @@ -114,7 +113,123 @@ describe("CSSLanguage", () => {
assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 1);
assert.strictEqual(result.errors[0].message, "Colon is expected");
assert.strictEqual(result.errors[0].message, "Missing closing }");
assert.strictEqual(result.errors[0].line, 1);
assert.strictEqual(result.errors[0].column, 3);
assert.strictEqual(result.errors[0].offset, 2);
});

it("should return an error when a CSS bad string is found", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "a { content: 'this\nstring is not properly closed' }",
path: "test.css",
});

assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 2);
assert.strictEqual(result.errors[0].message, "Missing closing }");
assert.strictEqual(result.errors[0].line, 1);
assert.strictEqual(result.errors[0].column, 3);
assert.strictEqual(result.errors[0].offset, 2);
assert.strictEqual(result.errors[1].message, "Unexpected input");
assert.strictEqual(result.errors[1].line, 1);
assert.strictEqual(result.errors[1].column, 14);
assert.strictEqual(result.errors[1].offset, 13);
});

it("should return an error when a CSS bad URL is found", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "a { background: url(foo bar.png) }",
path: "test.css",
});

assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 1);
assert.strictEqual(result.errors[0].message, "Unexpected input");
assert.strictEqual(result.errors[0].line, 1);
assert.strictEqual(result.errors[0].column, 17);
assert.strictEqual(result.errors[0].offset, 16);
});

it("should return an error when braces are unclosed", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "a { color: red;",
path: "test.css",
});

assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 1);
assert.strictEqual(result.errors[0].message, "Missing closing }");
assert.strictEqual(result.errors[0].line, 1);
assert.strictEqual(result.errors[0].column, 3);
assert.strictEqual(result.errors[0].offset, 2);
});

it("should return an error when square brackets are unclosed", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "a[foo { color: red; }",
path: "test.css",
});

assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 3); // other errors caused by the first one
assert.strictEqual(result.errors[0].message, "Missing closing ]");
assert.strictEqual(result.errors[0].line, 1);
assert.strictEqual(result.errors[0].column, 2);
assert.strictEqual(result.errors[0].offset, 1);
});

it("should return an error when parentheses are unclosed", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "@supports (color: red {}",
path: "test.css",
});

assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 2); // other errors caused by the first one
assert.strictEqual(result.errors[0].message, "Missing closing )");
assert.strictEqual(result.errors[0].line, 1);
assert.strictEqual(result.errors[0].column, 11);
assert.strictEqual(result.errors[0].offset, 10);
});

it("should return an error when function parentheses is unclosed", () => {
const language = new CSSLanguage();
const result = language.parse({
body: "a { width: min(40%, 400px; }",
path: "test.css",
});

assert.strictEqual(result.ok, false);
assert.strictEqual(result.ast, undefined);
assert.strictEqual(result.errors.length, 4); // other errors caused by the first one
assert.strictEqual(result.errors[1].message, "Missing closing )");
assert.strictEqual(result.errors[1].line, 1);
assert.strictEqual(result.errors[1].column, 12);
assert.strictEqual(result.errors[1].offset, 11);
});

it("should not return an error when braces are unclosed and tolerant: true is used", () => {
const language = new CSSLanguage();
const result = language.parse(
{
body: "a { color: red;",
path: "test.css",
},
{ languageOptions: { tolerant: true } },
);

assert.strictEqual(result.ok, true);
});
});

Expand Down