Skip to content

Commit

Permalink
Merge pull request #106 from Microsoft/octref/scss-unknown-at-rules
Browse files Browse the repository at this point in the history
 Handle unknown at rules. Fix #51
  • Loading branch information
octref authored Jun 18, 2018
2 parents aed9968 + e284752 commit 4aaa9e2
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 33 deletions.
24 changes: 22 additions & 2 deletions src/parser/cssNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ export enum NodeType {
SupportsCondition,
NamespacePrefix,
GridLine,
Plugin
Plugin,
UnknownAtRule,
}

export enum ReferenceType {
Expand Down Expand Up @@ -1242,7 +1243,7 @@ export class AttributeSelector extends Node {

public getValue(): BinaryExpression {
return this.value;
}
}
}

export class Operator extends Node {
Expand Down Expand Up @@ -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;
Expand Down
99 changes: 90 additions & 9 deletions src/parser/cssParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 = <nodes.UnknownAtRule>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 = <nodes.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('/') ||
Expand Down
16 changes: 8 additions & 8 deletions src/parser/cssScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export enum TokenType {
Percentage,
Dimension,
UnicodeRange,
CDO,
CDC,
CDO, // <!--
CDC, // -->
Colon,
SemiColon,
CurlyL,
Expand All @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions src/parser/lessParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 3 additions & 6 deletions src/parser/scssParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions src/services/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(<nodes.UnknownAtRule>node);
case nodes.NodeType.Keyframe:
return this.visitKeyframe(<nodes.Keyframe>node);
case nodes.NodeType.FontFace:
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/services/lintRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
18 changes: 16 additions & 2 deletions src/test/css/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions src/test/less/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
2 changes: 1 addition & 1 deletion src/test/scss/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down

0 comments on commit 4aaa9e2

Please sign in to comment.