diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 77eda9e9..09a96f70 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -81,7 +81,8 @@ export enum NodeType { SupportsCondition, NamespacePrefix, GridLine, - Plugin + Plugin, + UnknownAtRule, } export enum ReferenceType { @@ -1242,7 +1243,7 @@ export class AttributeSelector extends Node { public getValue(): BinaryExpression { return this.value; - } + } } export class Operator extends Node { @@ -1486,6 +1487,25 @@ export class MixinDeclaration extends BodyDeclaration { } } +export class UnknownAtRule extends BodyDeclaration { + public atRuleName: string; + + constructor(offset: number, length: number) { + super(offset, length); + } + + public get type(): NodeType { + return NodeType.UnknownAtRule; + } + + public setAtRuleName(atRuleName: string) { + this.atRuleName = atRuleName; + } + public getAtRuleName(atRuleName: string) { + return this.atRuleName; + } +} + export class ListEntry extends Node { public key?: Node; diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index 3fce1811..e0de3bf8 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -272,19 +272,24 @@ export class Parser { public _parseStylesheetStatement(): nodes.Node { if (this.peek(TokenType.AtKeyword)) { - return this._parseImport() - || this._parseMedia() - || this._parsePage() - || this._parseFontFace() - || this._parseKeyframe() - || this._parseSupports() - || this._parseViewPort() - || this._parseNamespace() - || this._parseDocument(); + return this._parseStylesheetAtStatement(); } return this._parseRuleset(false); } + public _parseStylesheetAtStatement(): nodes.Node { + return this._parseImport() + || this._parseMedia() + || this._parsePage() + || this._parseFontFace() + || this._parseKeyframe() + || this._parseSupports() + || this._parseViewPort() + || this._parseNamespace() + || this._parseDocument() + || this._parseUnknownAtRule(); + } + public _tryParseRuleset(isNested: boolean): nodes.RuleSet { let mark = this.mark(); if (this._parseSelector(isNested)) { @@ -992,6 +997,82 @@ export class Parser { return this._parseBody(node, this._parseStylesheetStatement.bind(this)); } + // https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule + public _parseUnknownAtRule(): nodes.Node { + let node = this.create(nodes.UnknownAtRule); + node.addChild(this._parseUnknownAtRuleName()); + + const isTopLevel = () => curlyDepth === 0 && parensDepth === 0 && bracketsDepth === 0; + let curlyDepth = 0; + let parensDepth = 0; + let bracketsDepth = 0; + done: while (true) { + switch (this.token.type) { + case TokenType.SemiColon: + if (isTopLevel()) { + break done; + } + break; + case TokenType.EOF: + if (curlyDepth > 0) { + return this.finish(node, ParseError.RightCurlyExpected); + } else if (bracketsDepth > 0) { + return this.finish(node, ParseError.RightSquareBracketExpected); + } else if (parensDepth > 0) { + return this.finish(node, ParseError.RightParenthesisExpected); + } else { + return this.finish(node); + } + case TokenType.CurlyL: + curlyDepth++; + break; + case TokenType.CurlyR: + curlyDepth--; + if (curlyDepth < 0) { + // The property value has been terminated without a semicolon, and + // this is the last declaration in the ruleset. + if (parensDepth === 0 && bracketsDepth === 0) { + break done; + } + return this.finish(node, ParseError.LeftCurlyExpected); + } + break; + case TokenType.ParenthesisL: + parensDepth++; + break; + case TokenType.ParenthesisR: + parensDepth--; + if (parensDepth < 0) { + return this.finish(node, ParseError.LeftParenthesisExpected); + } + break; + case TokenType.BracketL: + bracketsDepth++; + break; + case TokenType.BracketR: + bracketsDepth--; + if (bracketsDepth < 0) { + return this.finish(node, ParseError.LeftSquareBracketExpected); + } + break; + } + + this.consumeToken(); + } + + return node; + } + + public _parseUnknownAtRuleName(): nodes.Node { + let node = this.create(nodes.Node); + + if (this.accept(TokenType.AtKeyword)) { + return this.finish(node); + } + + return node; + } + public _parseOperator(): nodes.Operator { // these are operators for binary expressions if (this.peekDelim('/') || diff --git a/src/parser/cssScanner.ts b/src/parser/cssScanner.ts index bf7538f7..077f638c 100644 --- a/src/parser/cssScanner.ts +++ b/src/parser/cssScanner.ts @@ -15,8 +15,8 @@ export enum TokenType { Percentage, Dimension, UnicodeRange, - CDO, - CDC, + CDO, // Colon, SemiColon, CurlyL, @@ -27,13 +27,13 @@ export enum TokenType { BracketR, Whitespace, Includes, - Dashmatch, - SubstringOperator, - PrefixOperator, - SuffixOperator, + Dashmatch, // |= + SubstringOperator, // *= + PrefixOperator, // ^= + SuffixOperator, // $= Delim, - EMS, - EXS, + EMS, // 3em + EXS, // 3ex Length, Angle, Time, diff --git a/src/parser/lessParser.ts b/src/parser/lessParser.ts index d0faba9a..3a1d3bae 100644 --- a/src/parser/lessParser.ts +++ b/src/parser/lessParser.ts @@ -21,11 +21,15 @@ export class LESSParser extends cssParser.Parser { } public _parseStylesheetStatement(): nodes.Node { + if (this.peek(TokenType.AtKeyword)) { + return this._parseVariableDeclaration() + || this._parsePlugin() + || super._parseStylesheetAtStatement(); + } + return this._tryParseMixinDeclaration() || this._tryParseMixinReference(true) - || super._parseStylesheetStatement() - || this._parseVariableDeclaration() - || this._parsePlugin(); + || this._parseRuleset(true); } public _parseImport(): nodes.Node { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 4311e9f0..7f2fe34c 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -22,19 +22,16 @@ export class SCSSParser extends cssParser.Parser { } public _parseStylesheetStatement(): nodes.Node { - let node = super._parseStylesheetStatement(); - if (node) { - return node; - } if (this.peek(TokenType.AtKeyword)) { return this._parseWarnAndDebug() || this._parseControlStatement() || this._parseMixinDeclaration() || this._parseMixinContent() || this._parseMixinReference() // @include - || this._parseFunctionDeclaration(); + || this._parseFunctionDeclaration() + || super._parseStylesheetAtStatement(); } - return this._parseVariableDeclaration(); + return this._parseRuleset(true) || this._parseVariableDeclaration(); } public _parseImport(): nodes.Node { diff --git a/src/services/lint.ts b/src/services/lint.ts index 42c55a42..90cf393c 100644 --- a/src/services/lint.ts +++ b/src/services/lint.ts @@ -136,6 +136,8 @@ export class LintVisitor implements nodes.IVisitor { public visitNode(node: nodes.Node): boolean { switch (node.type) { + case nodes.NodeType.UnknownAtRule: + return this.visitUnknownAtRule(node); case nodes.NodeType.Keyframe: return this.visitKeyframe(node); case nodes.NodeType.FontFace: @@ -162,6 +164,16 @@ export class LintVisitor implements nodes.IVisitor { this.validateKeyframes(); } + private visitUnknownAtRule(node: nodes.UnknownAtRule): boolean { + const atRuleName = node.getChild(0); + if (!atRuleName) { + return false; + } + + this.addEntry(atRuleName, Rules.UnknownAtRules, `Unknown at rule ${atRuleName.getText()}`); + return true; + } + private visitKeyframe(node: nodes.Keyframe): boolean { let keyword = node.getKeyword(); let text = keyword.getText(); diff --git a/src/services/lintRules.ts b/src/services/lintRules.ts index d952f6bf..c9439949 100644 --- a/src/services/lintRules.ts +++ b/src/services/lintRules.ts @@ -35,6 +35,7 @@ export let Rules = { HexColorLength: new Rule('hexColorLength', localize('rule.hexColor', "Hex colors must consist of three, four, six or eight hex numbers"), Error), ArgsInColorFunction: new Rule('argumentsInColorFunction', localize('rule.colorFunction', "Invalid number of parameters"), Error), UnknownProperty: new Rule('unknownProperties', localize('rule.unknownProperty', "Unknown property."), Warning), + UnknownAtRules: new Rule('unknownAtRules', localize('rule.unknownAtRules', "Unknown at-rule."), Warning), IEStarHack: new Rule('ieHack', localize('rule.ieHack', "IE hacks are only necessary when supporting IE7 and older"), Ignore), UnknownVendorSpecificProperty: new Rule('unknownVendorSpecificProperties', localize('rule.unknownVendorSpecificProperty', "Unknown vendor specific property."), Ignore), PropertyIgnoredDueToDisplay: new Rule('propertyIgnoredDueToDisplay', localize('rule.propertyIgnoredDueToDisplay', "Property is ignored due to the display."), Warning), diff --git a/src/test/css/parser.test.ts b/src/test/css/parser.test.ts index 7c38acd5..c24016a8 100644 --- a/src/test/css/parser.test.ts +++ b/src/test/css/parser.test.ts @@ -71,11 +71,25 @@ suite('CSS - Parser', () => { assertNode('E.warning E#myid E:not(s) {}', parser, parser._parseStylesheet.bind(parser)); assertError('@namespace;', parser, parser._parseStylesheet.bind(parser), ParseError.URIExpected); assertError('@namespace url(http://test)', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected); - assertError('@mskeyframes darkWordHighlight { from { background-color: inherit; } to { background-color: rgba(83, 83, 83, 0.7); } }', parser, parser._parseStylesheet.bind(parser), ParseError.UnknownAtRule); assertError('@charset;', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); assertError('@charset \'utf8\'', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected); }); + test('stylesheet - graceful handling of unknown rules', function () { + let parser = new Parser(); + assertNode('@unknown-rule;', parser, parser._parseStylesheet.bind(parser)); + assertNode(`@unknown-rule 'foo';`, parser, parser._parseStylesheet.bind(parser)); + assertNode('@unknown-rule (foo) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@unknown-rule (foo) { .bar {} }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mskeyframes darkWordHighlight { from { background-color: inherit; } to { background-color: rgba(83, 83, 83, 0.7); } }', parser, parser._parseStylesheet.bind(parser)); + + assertError('@unknown-rule (;', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('@unknown-rule [foo', parser, parser._parseStylesheet.bind(parser), ParseError.RightSquareBracketExpected); + assertError('@unknown-rule { [foo }', parser, parser._parseStylesheet.bind(parser), ParseError.RightSquareBracketExpected); + assertError('@unknown-rule (foo) {', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertError('@unknown-rule (foo) { .bar {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + }); + test('stylesheet /panic/', function () { let parser = new Parser(); assertError('#boo, far } \n.far boo {}', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected); @@ -195,7 +209,7 @@ suite('CSS - Parser', () => { assertNode('@page { @top-right-corner { content: url(foo.png); border: solid green; } }', parser, parser._parsePage.bind(parser)); assertNode('@page { @top-left-corner { content: " "; border: solid green; } @bottom-right-corner { content: counter(page); border: solid green; } }', parser, parser._parsePage.bind(parser)); assertError('@page { @top-left-corner foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.LeftCurlyExpected); - assertError('@page { @XY foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.UnknownAtRule); + // assertError('@page { @XY foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.UnknownAtRule); assertError('@page :left { margin-left: 4cm margin-right: 3cm; }', parser, parser._parsePage.bind(parser), ParseError.SemiColonExpected); assertError('@page : { }', parser, parser._parsePage.bind(parser), ParseError.IdentifierExpected); assertError('@page :left, { }', parser, parser._parsePage.bind(parser), ParseError.IdentifierExpected); diff --git a/src/test/less/parser.test.ts b/src/test/less/parser.test.ts index 38215943..93be99b0 100644 --- a/src/test/less/parser.test.ts +++ b/src/test/less/parser.test.ts @@ -35,8 +35,7 @@ suite('LESS - Parser', () => { assertNode('.something { @media (max-width: 760px) { > div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('@media (@var) {}', parser, parser._parseMedia.bind(parser)); assertNode('@media screen and (@var) {}', parser, parser._parseMedia.bind(parser)); - - assertError('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertNode('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser)); }); test('VariableDeclaration', function () { diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 08d77bae..f2724238 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -188,7 +188,7 @@ suite('SCSS - Parser', () => { assertNode('.something { @media (max-width: 760px) { > .test { color: blue; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('.something { @media (max-width: 760px) { ~ div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('.something { @media (max-width: 760px) { + div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); - assertError('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertNode('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser)); }); test('@keyframe', function () {