Skip to content

Commit 3544cef

Browse files
committed
Add support for @error rules
1 parent 0df0a86 commit 3544cef

File tree

7 files changed

+385
-2
lines changed

7 files changed

+385
-2
lines changed

pkg/sass-parser/lib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export {
3939
DebugRuleRaws,
4040
} from './src/statement/debug-rule';
4141
export {EachRule, EachRuleProps, EachRuleRaws} from './src/statement/each-rule';
42+
export {
43+
ErrorRule,
44+
ErrorRuleProps,
45+
ErrorRuleRaws,
46+
} from './src/statement/error-rule';
4247
export {
4348
GenericAtRule,
4449
GenericAtRuleProps,

pkg/sass-parser/lib/src/sass-internal.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ declare namespace SassInternal {
8585
readonly list: Expression;
8686
}
8787

88+
class ErrorRule extends Statement {
89+
readonly expression: Expression;
90+
}
91+
8892
class Stylesheet extends ParentStatement<Statement[]> {}
8993

9094
class StyleRule extends ParentStatement<Statement[]> {
@@ -124,6 +128,7 @@ export type AtRootRule = SassInternal.AtRootRule;
124128
export type AtRule = SassInternal.AtRule;
125129
export type DebugRule = SassInternal.DebugRule;
126130
export type EachRule = SassInternal.EachRule;
131+
export type ErrorRule = SassInternal.ErrorRule;
127132
export type Stylesheet = SassInternal.Stylesheet;
128133
export type StyleRule = SassInternal.StyleRule;
129134
export type Interpolation = SassInternal.Interpolation;
@@ -136,6 +141,7 @@ export interface StatementVisitorObject<T> {
136141
visitAtRule(node: AtRule): T;
137142
visitDebugRule(node: DebugRule): T;
138143
visitEachRule(node: EachRule): T;
144+
visitErrorRule(node: ErrorRule): T;
139145
visitStyleRule(node: StyleRule): T;
140146
}
141147

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a @error rule toJSON 1`] = `
4+
{
5+
"errorExpression": <foo>,
6+
"inputs": [
7+
{
8+
"css": "@error foo",
9+
"hasBOM": false,
10+
"id": "<input css _____>",
11+
},
12+
],
13+
"name": "error",
14+
"params": "foo",
15+
"raws": {},
16+
"sassType": "error-rule",
17+
"source": <1:1-1:11 in 0>,
18+
"type": "atrule",
19+
}
20+
`;
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright 2024 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {ErrorRule, StringExpression, sass, scss} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a @error rule', () => {
9+
let node: ErrorRule;
10+
function describeNode(description: string, create: () => ErrorRule): void {
11+
describe(description, () => {
12+
beforeEach(() => void (node = create()));
13+
14+
it('has a name', () => expect(node.name.toString()).toBe('error'));
15+
16+
it('has an expression', () =>
17+
expect(node).toHaveStringExpression('errorExpression', 'foo'));
18+
19+
it('has matching params', () => expect(node.params).toBe('foo'));
20+
21+
it('has undefined nodes', () => expect(node.nodes).toBeUndefined());
22+
});
23+
}
24+
25+
describeNode(
26+
'parsed as SCSS',
27+
() => scss.parse('@error foo').nodes[0] as ErrorRule
28+
);
29+
30+
describeNode(
31+
'parsed as Sass',
32+
() => sass.parse('@error foo').nodes[0] as ErrorRule
33+
);
34+
35+
describeNode(
36+
'constructed manually',
37+
() =>
38+
new ErrorRule({
39+
errorExpression: {text: 'foo'},
40+
})
41+
);
42+
43+
describeNode('constructed from ChildProps', () =>
44+
utils.fromChildProps({
45+
errorExpression: {text: 'foo'},
46+
})
47+
);
48+
49+
it('throws an error when assigned a new name', () =>
50+
expect(
51+
() =>
52+
(new ErrorRule({
53+
errorExpression: {text: 'foo'},
54+
}).name = 'bar')
55+
).toThrow());
56+
57+
describe('assigned a new expression', () => {
58+
beforeEach(() => {
59+
node = scss.parse('@error foo').nodes[0] as ErrorRule;
60+
});
61+
62+
it('sets an empty string expression as undefined params', () => {
63+
node.params = undefined;
64+
expect(node.params).toBe('');
65+
expect(node).toHaveStringExpression('errorExpression', '');
66+
});
67+
68+
it('sets an empty string expression as empty string params', () => {
69+
node.params = '';
70+
expect(node.params).toBe('');
71+
expect(node).toHaveStringExpression('errorExpression', '');
72+
});
73+
74+
it("removes the old expression's parent", () => {
75+
const oldExpression = node.errorExpression;
76+
node.errorExpression = {text: 'bar'};
77+
expect(oldExpression.parent).toBeUndefined();
78+
});
79+
80+
it("assigns the new expression's parent", () => {
81+
const expression = new StringExpression({text: 'bar'});
82+
node.errorExpression = expression;
83+
expect(expression.parent).toBe(node);
84+
});
85+
86+
it('assigns the expression explicitly', () => {
87+
const expression = new StringExpression({text: 'bar'});
88+
node.errorExpression = expression;
89+
expect(node.errorExpression).toBe(expression);
90+
});
91+
92+
it('assigns the expression as ExpressionProps', () => {
93+
node.errorExpression = {text: 'bar'};
94+
expect(node).toHaveStringExpression('errorExpression', 'bar');
95+
});
96+
97+
it('assigns the expression as params', () => {
98+
node.params = 'bar';
99+
expect(node).toHaveStringExpression('errorExpression', 'bar');
100+
});
101+
});
102+
103+
describe('stringifies', () => {
104+
describe('to SCSS', () => {
105+
it('with default raws', () =>
106+
expect(
107+
new ErrorRule({
108+
errorExpression: {text: 'foo'},
109+
}).toString()
110+
).toBe('@error foo;'));
111+
112+
it('with afterName', () =>
113+
expect(
114+
new ErrorRule({
115+
errorExpression: {text: 'foo'},
116+
raws: {afterName: '/**/'},
117+
}).toString()
118+
).toBe('@error/**/foo;'));
119+
120+
it('with between', () =>
121+
expect(
122+
new ErrorRule({
123+
errorExpression: {text: 'foo'},
124+
raws: {between: '/**/'},
125+
}).toString()
126+
).toBe('@error foo/**/;'));
127+
});
128+
});
129+
130+
describe('clone', () => {
131+
let original: ErrorRule;
132+
beforeEach(() => {
133+
original = scss.parse('@error foo').nodes[0] as ErrorRule;
134+
// TODO: remove this once raws are properly parsed
135+
original.raws.between = ' ';
136+
});
137+
138+
describe('with no overrides', () => {
139+
let clone: ErrorRule;
140+
beforeEach(() => void (clone = original.clone()));
141+
142+
describe('has the same properties:', () => {
143+
it('params', () => expect(clone.params).toBe('foo'));
144+
145+
it('errorExpression', () =>
146+
expect(clone).toHaveStringExpression('errorExpression', 'foo'));
147+
148+
it('raws', () => expect(clone.raws).toEqual({between: ' '}));
149+
150+
it('source', () => expect(clone.source).toBe(original.source));
151+
});
152+
153+
describe('creates a new', () => {
154+
it('self', () => expect(clone).not.toBe(original));
155+
156+
for (const attr of ['errorExpression', 'raws'] as const) {
157+
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
158+
}
159+
});
160+
});
161+
162+
describe('overrides', () => {
163+
describe('raws', () => {
164+
it('defined', () =>
165+
expect(original.clone({raws: {afterName: ' '}}).raws).toEqual({
166+
afterName: ' ',
167+
}));
168+
169+
it('undefined', () =>
170+
expect(original.clone({raws: undefined}).raws).toEqual({
171+
between: ' ',
172+
}));
173+
});
174+
175+
describe('errorExpression', () => {
176+
describe('defined', () => {
177+
let clone: ErrorRule;
178+
beforeEach(() => {
179+
clone = original.clone({errorExpression: {text: 'bar'}});
180+
});
181+
182+
it('changes params', () => expect(clone.params).toBe('bar'));
183+
184+
it('changes errorExpression', () =>
185+
expect(clone).toHaveStringExpression('errorExpression', 'bar'));
186+
});
187+
188+
describe('undefined', () => {
189+
let clone: ErrorRule;
190+
beforeEach(() => {
191+
clone = original.clone({errorExpression: undefined});
192+
});
193+
194+
it('preserves params', () => expect(clone.params).toBe('foo'));
195+
196+
it('preserves errorExpression', () =>
197+
expect(clone).toHaveStringExpression('errorExpression', 'foo'));
198+
});
199+
});
200+
});
201+
});
202+
203+
it('toJSON', () =>
204+
expect(scss.parse('@error foo').nodes[0]).toMatchSnapshot());
205+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2024 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import * as postcss from 'postcss';
6+
import type {AtRuleRaws as PostcssAtRuleRaws} from 'postcss/lib/at-rule';
7+
8+
import {convertExpression} from '../expression/convert';
9+
import {Expression, ExpressionProps} from '../expression';
10+
import {fromProps} from '../expression/from-props';
11+
import {LazySource} from '../lazy-source';
12+
import type * as sassInternal from '../sass-internal';
13+
import * as utils from '../utils';
14+
import {Statement, StatementWithChildren} from '.';
15+
import {_AtRule} from './at-rule-internal';
16+
import {interceptIsClean} from './intercept-is-clean';
17+
import * as sassParser from '../..';
18+
19+
/**
20+
* The set of raws supported by {@link ErrorRule}.
21+
*
22+
* @category Statement
23+
*/
24+
export type ErrorRuleRaws = Pick<
25+
PostcssAtRuleRaws,
26+
'afterName' | 'before' | 'between'
27+
>;
28+
29+
/**
30+
* The initializer properties for {@link ErrorRule}.
31+
*
32+
* @category Statement
33+
*/
34+
export type ErrorRuleProps = postcss.NodeProps & {
35+
raws?: ErrorRuleRaws;
36+
errorExpression: Expression | ExpressionProps;
37+
};
38+
39+
/**
40+
* An `@error` rule. Extends [`postcss.AtRule`].
41+
*
42+
* [`postcss.AtRule`]: https://postcss.org/api/#atrule
43+
*
44+
* @category Statement
45+
*/
46+
export class ErrorRule
47+
extends _AtRule<Partial<ErrorRuleProps>>
48+
implements Statement
49+
{
50+
readonly sassType = 'error-rule' as const;
51+
declare parent: StatementWithChildren | undefined;
52+
declare raws: ErrorRuleRaws;
53+
declare readonly nodes: undefined;
54+
55+
get name(): string {
56+
return 'error';
57+
}
58+
set name(value: string) {
59+
throw new Error("ErrorRule.name can't be overwritten.");
60+
}
61+
62+
get params(): string {
63+
return this.errorExpression.toString();
64+
}
65+
set params(value: string | number | undefined) {
66+
this.errorExpression = {text: value?.toString() ?? ''};
67+
}
68+
69+
/** The expresison whose value is thrown when the error rule is executed. */
70+
get errorExpression(): Expression {
71+
return this._errorExpression!;
72+
}
73+
set errorExpression(errorExpression: Expression | ExpressionProps) {
74+
if (this._errorExpression) this._errorExpression.parent = undefined;
75+
if (!('sassType' in errorExpression)) {
76+
errorExpression = fromProps(errorExpression);
77+
}
78+
if (errorExpression) errorExpression.parent = this;
79+
this._errorExpression = errorExpression;
80+
}
81+
private _errorExpression?: Expression;
82+
83+
constructor(defaults: ErrorRuleProps);
84+
/** @hidden */
85+
constructor(_: undefined, inner: sassInternal.ErrorRule);
86+
constructor(defaults?: ErrorRuleProps, inner?: sassInternal.ErrorRule) {
87+
super(defaults as unknown as postcss.AtRuleProps);
88+
89+
if (inner) {
90+
this.source = new LazySource(inner);
91+
this.errorExpression = convertExpression(inner.expression);
92+
}
93+
}
94+
95+
clone(overrides?: Partial<ErrorRuleProps>): this {
96+
return utils.cloneNode(
97+
this,
98+
overrides,
99+
['raws', 'errorExpression'],
100+
[{name: 'params', explicitUndefined: true}]
101+
);
102+
}
103+
104+
toJSON(): object;
105+
/** @hidden */
106+
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
107+
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
108+
return utils.toJSON(
109+
this,
110+
['name', 'errorExpression', 'params', 'nodes'],
111+
inputs
112+
);
113+
}
114+
115+
/** @hidden */
116+
toString(
117+
stringifier: postcss.Stringifier | postcss.Syntax = sassParser.scss
118+
.stringify
119+
): string {
120+
return super.toString(stringifier);
121+
}
122+
123+
/** @hidden */
124+
get nonStatementChildren(): ReadonlyArray<Expression> {
125+
return [this.errorExpression];
126+
}
127+
}
128+
129+
interceptIsClean(ErrorRule);

0 commit comments

Comments
 (0)