Skip to content

Commit 4aaa9e2

Browse files
authored
Merge pull request #106 from Microsoft/octref/scss-unknown-at-rules
Handle unknown at rules. Fix #51
2 parents aed9968 + e284752 commit 4aaa9e2

File tree

10 files changed

+161
-33
lines changed

10 files changed

+161
-33
lines changed

src/parser/cssNodes.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ export enum NodeType {
8181
SupportsCondition,
8282
NamespacePrefix,
8383
GridLine,
84-
Plugin
84+
Plugin,
85+
UnknownAtRule,
8586
}
8687

8788
export enum ReferenceType {
@@ -1242,7 +1243,7 @@ export class AttributeSelector extends Node {
12421243

12431244
public getValue(): BinaryExpression {
12441245
return this.value;
1245-
}
1246+
}
12461247
}
12471248

12481249
export class Operator extends Node {
@@ -1486,6 +1487,25 @@ export class MixinDeclaration extends BodyDeclaration {
14861487
}
14871488
}
14881489

1490+
export class UnknownAtRule extends BodyDeclaration {
1491+
public atRuleName: string;
1492+
1493+
constructor(offset: number, length: number) {
1494+
super(offset, length);
1495+
}
1496+
1497+
public get type(): NodeType {
1498+
return NodeType.UnknownAtRule;
1499+
}
1500+
1501+
public setAtRuleName(atRuleName: string) {
1502+
this.atRuleName = atRuleName;
1503+
}
1504+
public getAtRuleName(atRuleName: string) {
1505+
return this.atRuleName;
1506+
}
1507+
}
1508+
14891509
export class ListEntry extends Node {
14901510

14911511
public key?: Node;

src/parser/cssParser.ts

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,19 +272,24 @@ export class Parser {
272272

273273
public _parseStylesheetStatement(): nodes.Node {
274274
if (this.peek(TokenType.AtKeyword)) {
275-
return this._parseImport()
276-
|| this._parseMedia()
277-
|| this._parsePage()
278-
|| this._parseFontFace()
279-
|| this._parseKeyframe()
280-
|| this._parseSupports()
281-
|| this._parseViewPort()
282-
|| this._parseNamespace()
283-
|| this._parseDocument();
275+
return this._parseStylesheetAtStatement();
284276
}
285277
return this._parseRuleset(false);
286278
}
287279

280+
public _parseStylesheetAtStatement(): nodes.Node {
281+
return this._parseImport()
282+
|| this._parseMedia()
283+
|| this._parsePage()
284+
|| this._parseFontFace()
285+
|| this._parseKeyframe()
286+
|| this._parseSupports()
287+
|| this._parseViewPort()
288+
|| this._parseNamespace()
289+
|| this._parseDocument()
290+
|| this._parseUnknownAtRule();
291+
}
292+
288293
public _tryParseRuleset(isNested: boolean): nodes.RuleSet {
289294
let mark = this.mark();
290295
if (this._parseSelector(isNested)) {
@@ -992,6 +997,82 @@ export class Parser {
992997
return this._parseBody(node, this._parseStylesheetStatement.bind(this));
993998
}
994999

1000+
// https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule
1001+
public _parseUnknownAtRule(): nodes.Node {
1002+
let node = <nodes.UnknownAtRule>this.create(nodes.UnknownAtRule);
1003+
node.addChild(this._parseUnknownAtRuleName());
1004+
1005+
const isTopLevel = () => curlyDepth === 0 && parensDepth === 0 && bracketsDepth === 0;
1006+
let curlyDepth = 0;
1007+
let parensDepth = 0;
1008+
let bracketsDepth = 0;
1009+
done: while (true) {
1010+
switch (this.token.type) {
1011+
case TokenType.SemiColon:
1012+
if (isTopLevel()) {
1013+
break done;
1014+
}
1015+
break;
1016+
case TokenType.EOF:
1017+
if (curlyDepth > 0) {
1018+
return this.finish(node, ParseError.RightCurlyExpected);
1019+
} else if (bracketsDepth > 0) {
1020+
return this.finish(node, ParseError.RightSquareBracketExpected);
1021+
} else if (parensDepth > 0) {
1022+
return this.finish(node, ParseError.RightParenthesisExpected);
1023+
} else {
1024+
return this.finish(node);
1025+
}
1026+
case TokenType.CurlyL:
1027+
curlyDepth++;
1028+
break;
1029+
case TokenType.CurlyR:
1030+
curlyDepth--;
1031+
if (curlyDepth < 0) {
1032+
// The property value has been terminated without a semicolon, and
1033+
// this is the last declaration in the ruleset.
1034+
if (parensDepth === 0 && bracketsDepth === 0) {
1035+
break done;
1036+
}
1037+
return this.finish(node, ParseError.LeftCurlyExpected);
1038+
}
1039+
break;
1040+
case TokenType.ParenthesisL:
1041+
parensDepth++;
1042+
break;
1043+
case TokenType.ParenthesisR:
1044+
parensDepth--;
1045+
if (parensDepth < 0) {
1046+
return this.finish(node, ParseError.LeftParenthesisExpected);
1047+
}
1048+
break;
1049+
case TokenType.BracketL:
1050+
bracketsDepth++;
1051+
break;
1052+
case TokenType.BracketR:
1053+
bracketsDepth--;
1054+
if (bracketsDepth < 0) {
1055+
return this.finish(node, ParseError.LeftSquareBracketExpected);
1056+
}
1057+
break;
1058+
}
1059+
1060+
this.consumeToken();
1061+
}
1062+
1063+
return node;
1064+
}
1065+
1066+
public _parseUnknownAtRuleName(): nodes.Node {
1067+
let node = <nodes.Node>this.create(nodes.Node);
1068+
1069+
if (this.accept(TokenType.AtKeyword)) {
1070+
return this.finish(node);
1071+
}
1072+
1073+
return node;
1074+
}
1075+
9951076
public _parseOperator(): nodes.Operator {
9961077
// these are operators for binary expressions
9971078
if (this.peekDelim('/') ||

src/parser/cssScanner.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export enum TokenType {
1515
Percentage,
1616
Dimension,
1717
UnicodeRange,
18-
CDO,
19-
CDC,
18+
CDO, // <!--
19+
CDC, // -->
2020
Colon,
2121
SemiColon,
2222
CurlyL,
@@ -27,13 +27,13 @@ export enum TokenType {
2727
BracketR,
2828
Whitespace,
2929
Includes,
30-
Dashmatch,
31-
SubstringOperator,
32-
PrefixOperator,
33-
SuffixOperator,
30+
Dashmatch, // |=
31+
SubstringOperator, // *=
32+
PrefixOperator, // ^=
33+
SuffixOperator, // $=
3434
Delim,
35-
EMS,
36-
EXS,
35+
EMS, // 3em
36+
EXS, // 3ex
3737
Length,
3838
Angle,
3939
Time,

src/parser/lessParser.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ export class LESSParser extends cssParser.Parser {
2121
}
2222

2323
public _parseStylesheetStatement(): nodes.Node {
24+
if (this.peek(TokenType.AtKeyword)) {
25+
return this._parseVariableDeclaration()
26+
|| this._parsePlugin()
27+
|| super._parseStylesheetAtStatement();
28+
}
29+
2430
return this._tryParseMixinDeclaration()
2531
|| this._tryParseMixinReference(true)
26-
|| super._parseStylesheetStatement()
27-
|| this._parseVariableDeclaration()
28-
|| this._parsePlugin();
32+
|| this._parseRuleset(true);
2933
}
3034

3135
public _parseImport(): nodes.Node {

src/parser/scssParser.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,16 @@ export class SCSSParser extends cssParser.Parser {
2222
}
2323

2424
public _parseStylesheetStatement(): nodes.Node {
25-
let node = super._parseStylesheetStatement();
26-
if (node) {
27-
return node;
28-
}
2925
if (this.peek(TokenType.AtKeyword)) {
3026
return this._parseWarnAndDebug()
3127
|| this._parseControlStatement()
3228
|| this._parseMixinDeclaration()
3329
|| this._parseMixinContent()
3430
|| this._parseMixinReference() // @include
35-
|| this._parseFunctionDeclaration();
31+
|| this._parseFunctionDeclaration()
32+
|| super._parseStylesheetAtStatement();
3633
}
37-
return this._parseVariableDeclaration();
34+
return this._parseRuleset(true) || this._parseVariableDeclaration();
3835
}
3936

4037
public _parseImport(): nodes.Node {

src/services/lint.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export class LintVisitor implements nodes.IVisitor {
136136

137137
public visitNode(node: nodes.Node): boolean {
138138
switch (node.type) {
139+
case nodes.NodeType.UnknownAtRule:
140+
return this.visitUnknownAtRule(<nodes.UnknownAtRule>node);
139141
case nodes.NodeType.Keyframe:
140142
return this.visitKeyframe(<nodes.Keyframe>node);
141143
case nodes.NodeType.FontFace:
@@ -162,6 +164,16 @@ export class LintVisitor implements nodes.IVisitor {
162164
this.validateKeyframes();
163165
}
164166

167+
private visitUnknownAtRule(node: nodes.UnknownAtRule): boolean {
168+
const atRuleName = node.getChild(0);
169+
if (!atRuleName) {
170+
return false;
171+
}
172+
173+
this.addEntry(atRuleName, Rules.UnknownAtRules, `Unknown at rule ${atRuleName.getText()}`);
174+
return true;
175+
}
176+
165177
private visitKeyframe(node: nodes.Keyframe): boolean {
166178
let keyword = node.getKeyword();
167179
let text = keyword.getText();

src/services/lintRules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export let Rules = {
3535
HexColorLength: new Rule('hexColorLength', localize('rule.hexColor', "Hex colors must consist of three, four, six or eight hex numbers"), Error),
3636
ArgsInColorFunction: new Rule('argumentsInColorFunction', localize('rule.colorFunction', "Invalid number of parameters"), Error),
3737
UnknownProperty: new Rule('unknownProperties', localize('rule.unknownProperty', "Unknown property."), Warning),
38+
UnknownAtRules: new Rule('unknownAtRules', localize('rule.unknownAtRules', "Unknown at-rule."), Warning),
3839
IEStarHack: new Rule('ieHack', localize('rule.ieHack', "IE hacks are only necessary when supporting IE7 and older"), Ignore),
3940
UnknownVendorSpecificProperty: new Rule('unknownVendorSpecificProperties', localize('rule.unknownVendorSpecificProperty', "Unknown vendor specific property."), Ignore),
4041
PropertyIgnoredDueToDisplay: new Rule('propertyIgnoredDueToDisplay', localize('rule.propertyIgnoredDueToDisplay', "Property is ignored due to the display."), Warning),

src/test/css/parser.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,25 @@ suite('CSS - Parser', () => {
7171
assertNode('E.warning E#myid E:not(s) {}', parser, parser._parseStylesheet.bind(parser));
7272
assertError('@namespace;', parser, parser._parseStylesheet.bind(parser), ParseError.URIExpected);
7373
assertError('@namespace url(http://test)', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
74-
assertError('@mskeyframes darkWordHighlight { from { background-color: inherit; } to { background-color: rgba(83, 83, 83, 0.7); } }', parser, parser._parseStylesheet.bind(parser), ParseError.UnknownAtRule);
7574
assertError('@charset;', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
7675
assertError('@charset \'utf8\'', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
7776
});
7877

78+
test('stylesheet - graceful handling of unknown rules', function () {
79+
let parser = new Parser();
80+
assertNode('@unknown-rule;', parser, parser._parseStylesheet.bind(parser));
81+
assertNode(`@unknown-rule 'foo';`, parser, parser._parseStylesheet.bind(parser));
82+
assertNode('@unknown-rule (foo) {}', parser, parser._parseStylesheet.bind(parser));
83+
assertNode('@unknown-rule (foo) { .bar {} }', parser, parser._parseStylesheet.bind(parser));
84+
assertNode('@mskeyframes darkWordHighlight { from { background-color: inherit; } to { background-color: rgba(83, 83, 83, 0.7); } }', parser, parser._parseStylesheet.bind(parser));
85+
86+
assertError('@unknown-rule (;', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
87+
assertError('@unknown-rule [foo', parser, parser._parseStylesheet.bind(parser), ParseError.RightSquareBracketExpected);
88+
assertError('@unknown-rule { [foo }', parser, parser._parseStylesheet.bind(parser), ParseError.RightSquareBracketExpected);
89+
assertError('@unknown-rule (foo) {', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
90+
assertError('@unknown-rule (foo) { .bar {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
91+
});
92+
7993
test('stylesheet /panic/', function () {
8094
let parser = new Parser();
8195
assertError('#boo, far } \n.far boo {}', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected);
@@ -195,7 +209,7 @@ suite('CSS - Parser', () => {
195209
assertNode('@page { @top-right-corner { content: url(foo.png); border: solid green; } }', parser, parser._parsePage.bind(parser));
196210
assertNode('@page { @top-left-corner { content: " "; border: solid green; } @bottom-right-corner { content: counter(page); border: solid green; } }', parser, parser._parsePage.bind(parser));
197211
assertError('@page { @top-left-corner foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.LeftCurlyExpected);
198-
assertError('@page { @XY foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.UnknownAtRule);
212+
// assertError('@page { @XY foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.UnknownAtRule);
199213
assertError('@page :left { margin-left: 4cm margin-right: 3cm; }', parser, parser._parsePage.bind(parser), ParseError.SemiColonExpected);
200214
assertError('@page : { }', parser, parser._parsePage.bind(parser), ParseError.IdentifierExpected);
201215
assertError('@page :left, { }', parser, parser._parsePage.bind(parser), ParseError.IdentifierExpected);

src/test/less/parser.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ suite('LESS - Parser', () => {
3535
assertNode('.something { @media (max-width: 760px) { > div { display: block; } } }', parser, parser._parseStylesheet.bind(parser));
3636
assertNode('@media (@var) {}', parser, parser._parseMedia.bind(parser));
3737
assertNode('@media screen and (@var) {}', parser, parser._parseMedia.bind(parser));
38-
39-
assertError('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
38+
assertNode('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser));
4039
});
4140

4241
test('VariableDeclaration', function () {

src/test/scss/parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ suite('SCSS - Parser', () => {
188188
assertNode('.something { @media (max-width: 760px) { > .test { color: blue; } } }', parser, parser._parseStylesheet.bind(parser));
189189
assertNode('.something { @media (max-width: 760px) { ~ div { display: block; } } }', parser, parser._parseStylesheet.bind(parser));
190190
assertNode('.something { @media (max-width: 760px) { + div { display: block; } } }', parser, parser._parseStylesheet.bind(parser));
191-
assertError('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
191+
assertNode('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser));
192192
});
193193

194194
test('@keyframe', function () {

0 commit comments

Comments
 (0)