Skip to content

Commit

Permalink
Add sass-parser support for the @warn rule
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Oct 24, 2024
1 parent 3ece3bd commit c43eaab
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 3 deletions.
2 changes: 2 additions & 0 deletions pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Add support for parsing variable declarations.

* Add support for parsing the `@warn` rule.

## 0.4.1

* Add `BooleanExpression` and `NumberExpression`.
Expand Down
1 change: 1 addition & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export {
VariableDeclarationProps,
VariableDeclarationRaws,
} from './src/statement/variable-declaration';
export {WarnRule, WarnRuleProps, WarnRuleRaws} from './src/statement/warn-rule';

/** Options that can be passed to the Sass parsers to control their behavior. */
export type SassParserOptions = Pick<postcss.ProcessOptions, 'from' | 'map'>;
Expand Down
6 changes: 6 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ declare namespace SassInternal {
readonly isGlobal: boolean;
}

class WarnRule extends Statement {
readonly expression: Expression;
}

class ConfiguredVariable extends SassNode {
readonly name: string;
readonly expression: Expression;
Expand Down Expand Up @@ -247,6 +251,7 @@ export type StyleRule = SassInternal.StyleRule;
export type SupportsRule = SassInternal.SupportsRule;
export type UseRule = SassInternal.UseRule;
export type VariableDeclaration = SassInternal.VariableDeclaration;
export type WarnRule = SassInternal.WarnRule;
export type ConfiguredVariable = SassInternal.ConfiguredVariable;
export type Interpolation = SassInternal.Interpolation;
export type Expression = SassInternal.Expression;
Expand All @@ -270,6 +275,7 @@ export interface StatementVisitorObject<T> {
visitSupportsRule(node: SupportsRule): T;
visitUseRule(node: UseRule): T;
visitVariableDeclaration(node: VariableDeclaration): T;
visitWarnRule(node: DebugRule): T;
}

export interface ExpressionVisitorObject<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a @warn rule toJSON 1`] = `
{
"inputs": [
{
"css": "@warn foo",
"hasBOM": false,
"id": "<input css _____>",
},
],
"name": "warn",
"params": "foo",
"raws": {},
"sassType": "warn-rule",
"source": <1:1-1:10 in 0>,
"type": "atrule",
"warnExpression": <foo>,
}
`;
13 changes: 10 additions & 3 deletions pkg/sass-parser/lib/src/statement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
VariableDeclaration,
VariableDeclarationProps,
} from './variable-declaration';
import {WarnRule, WarnRuleProps} from './warn-rule';

// TODO: Replace this with the corresponding Sass types once they're
// implemented.
Expand Down Expand Up @@ -54,7 +55,8 @@ export type StatementType =
| 'error-rule'
| 'use-rule'
| 'sass-comment'
| 'variable-declaration';
| 'variable-declaration'
| 'warn-rule';

/**
* All Sass statements that are also at-rules.
Expand All @@ -67,7 +69,8 @@ export type AtRule =
| ErrorRule
| ForRule
| GenericAtRule
| UseRule;
| UseRule
| WarnRule;

/**
* All Sass statements that are comments.
Expand Down Expand Up @@ -103,7 +106,8 @@ export type ChildProps =
| RuleProps
| SassCommentChildProps
| UseRuleProps
| VariableDeclarationProps;
| VariableDeclarationProps
| WarnRuleProps;

/**
* The Sass eqivalent of PostCSS's `ContainerProps`.
Expand Down Expand Up @@ -192,6 +196,7 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
},
visitUseRule: inner => new UseRule(undefined, inner),
visitVariableDeclaration: inner => new VariableDeclaration(undefined, inner),
visitWarnRule: inner => new WarnRule(undefined, inner),
});

/** Appends parsed versions of `internal`'s children to `container`. */
Expand Down Expand Up @@ -310,6 +315,8 @@ export function normalize(
result.push(new UseRule(node));
} else if ('variableName' in node) {
result.push(new VariableDeclaration(node));
} else if ('warnExpression' in node) {
result.push(new WarnRule(node));
} else {
result.push(...postcssNormalizeAndConvertToSass(self, node, sample));
}
Expand Down
205 changes: 205 additions & 0 deletions pkg/sass-parser/lib/src/statement/warn-rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import {StringExpression, WarnRule, sass, scss} from '../..';
import * as utils from '../../../test/utils';

describe('a @warn rule', () => {
let node: WarnRule;
function describeNode(description: string, create: () => WarnRule): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has a name', () => expect(node.name.toString()).toBe('warn'));

it('has an expression', () =>
expect(node).toHaveStringExpression('warnExpression', 'foo'));

it('has matching params', () => expect(node.params).toBe('foo'));

it('has undefined nodes', () => expect(node.nodes).toBeUndefined());
});
}

describeNode(
'parsed as SCSS',
() => scss.parse('@warn foo').nodes[0] as WarnRule
);

describeNode(
'parsed as Sass',
() => sass.parse('@warn foo').nodes[0] as WarnRule
);

describeNode(
'constructed manually',
() =>
new WarnRule({
warnExpression: {text: 'foo'},
})
);

describeNode('constructed from ChildProps', () =>
utils.fromChildProps({
warnExpression: {text: 'foo'},
})
);

it('throws an error when assigned a new name', () =>
expect(
() =>
(new WarnRule({
warnExpression: {text: 'foo'},
}).name = 'bar')
).toThrow());

describe('assigned a new expression', () => {
beforeEach(() => {
node = scss.parse('@warn foo').nodes[0] as WarnRule;
});

it('sets an empty string expression as undefined params', () => {
node.params = undefined;
expect(node.params).toBe('');
expect(node).toHaveStringExpression('warnExpression', '');
});

it('sets an empty string expression as empty string params', () => {
node.params = '';
expect(node.params).toBe('');
expect(node).toHaveStringExpression('warnExpression', '');
});

it("removes the old expression's parent", () => {
const oldExpression = node.warnExpression;
node.warnExpression = {text: 'bar'};
expect(oldExpression.parent).toBeUndefined();
});

it("assigns the new expression's parent", () => {
const expression = new StringExpression({text: 'bar'});
node.warnExpression = expression;
expect(expression.parent).toBe(node);
});

it('assigns the expression explicitly', () => {
const expression = new StringExpression({text: 'bar'});
node.warnExpression = expression;
expect(node.warnExpression).toBe(expression);
});

it('assigns the expression as ExpressionProps', () => {
node.warnExpression = {text: 'bar'};
expect(node).toHaveStringExpression('warnExpression', 'bar');
});

it('assigns the expression as params', () => {
node.params = 'bar';
expect(node).toHaveStringExpression('warnExpression', 'bar');
});
});

describe('stringifies', () => {
describe('to SCSS', () => {
it('with default raws', () =>
expect(
new WarnRule({
warnExpression: {text: 'foo'},
}).toString()
).toBe('@warn foo;'));

it('with afterName', () =>
expect(
new WarnRule({
warnExpression: {text: 'foo'},
raws: {afterName: '/**/'},
}).toString()
).toBe('@warn/**/foo;'));

it('with between', () =>
expect(
new WarnRule({
warnExpression: {text: 'foo'},
raws: {between: '/**/'},
}).toString()
).toBe('@warn foo/**/;'));
});
});

describe('clone', () => {
let original: WarnRule;
beforeEach(() => {
original = scss.parse('@warn foo').nodes[0] as WarnRule;
// TODO: remove this once raws are properly parsed
original.raws.between = ' ';
});

describe('with no overrides', () => {
let clone: WarnRule;
beforeEach(() => void (clone = original.clone()));

describe('has the same properties:', () => {
it('params', () => expect(clone.params).toBe('foo'));

it('warnExpression', () =>
expect(clone).toHaveStringExpression('warnExpression', 'foo'));

it('raws', () => expect(clone.raws).toEqual({between: ' '}));

it('source', () => expect(clone.source).toBe(original.source));
});

describe('creates a new', () => {
it('self', () => expect(clone).not.toBe(original));

for (const attr of ['warnExpression', 'raws'] as const) {
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
}
});
});

describe('overrides', () => {
describe('raws', () => {
it('defined', () =>
expect(original.clone({raws: {afterName: ' '}}).raws).toEqual({
afterName: ' ',
}));

it('undefined', () =>
expect(original.clone({raws: undefined}).raws).toEqual({
between: ' ',
}));
});

describe('warnExpression', () => {
describe('defined', () => {
let clone: WarnRule;
beforeEach(() => {
clone = original.clone({warnExpression: {text: 'bar'}});
});

it('changes params', () => expect(clone.params).toBe('bar'));

it('changes warnExpression', () =>
expect(clone).toHaveStringExpression('warnExpression', 'bar'));
});

describe('undefined', () => {
let clone: WarnRule;
beforeEach(() => {
clone = original.clone({warnExpression: undefined});
});

it('preserves params', () => expect(clone.params).toBe('foo'));

it('preserves warnExpression', () =>
expect(clone).toHaveStringExpression('warnExpression', 'foo'));
});
});
});
});

it('toJSON', () =>
expect(scss.parse('@warn foo').nodes[0]).toMatchSnapshot());
});
Loading

0 comments on commit c43eaab

Please sign in to comment.