Skip to content

Commit dc68aaa

Browse files
authored
parsers : do not walk sub trees of removed nodes and make mutations during AST walks safe (#1203)
1 parent 14bf9be commit dc68aaa

File tree

18 files changed

+887
-158
lines changed

18 files changed

+887
-158
lines changed

packages/css-parser-algorithms/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# Changes to CSS Parser Algorithms
22

3-
### Unreleased (patch)
3+
### Unreleased (minor)
44

55
- Small fixes in type definitions
6+
- Only `walk` child nodes if they are still part of the current AST tree [#1202](https://github.com/csstools/postcss-plugins/issues/1202)
7+
- Make `walk` methods safe for mutations [#1204](https://github.com/csstools/postcss-plugins/issues/1204)
8+
- Add a `forEach` method to `FunctionNode` and `SimpleBlockNode`
69

710
### 2.3.2
811

packages/css-parser-algorithms/dist/consume/consume-component-block-function.d.ts

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,129 @@ export declare function consumeComponentValue(ctx: Context, tokens: Array<CSSTok
77
advance: number;
88
node: ComponentValue;
99
};
10-
export declare class FunctionNode {
10+
declare abstract class ContainerNodeBaseClass {
11+
/**
12+
* The contents of the `Function` or `Simple Block`.
13+
* This is a list of component values.
14+
*/
15+
value: Array<ComponentValue>;
16+
/**
17+
* Retrieve the index of the given item in the current node.
18+
* For most node types this will be trivially implemented as `this.value.indexOf(item)`.
19+
*/
20+
indexOf(item: ComponentValue): number | string;
21+
/**
22+
* Retrieve the item at the given index in the current node.
23+
* For most node types this will be trivially implemented as `this.value[index]`.
24+
*/
25+
at(index: number | string): ComponentValue | undefined;
26+
/**
27+
* Iterates over each item in the `value` array of the current node.
28+
*
29+
* @param cb - The callback function to execute for each item.
30+
* The function receives an object containing the current node (`node`), its parent (`parent`),
31+
* and an optional `state` object.
32+
* A second parameter is the index of the current node.
33+
* The function can return `false` to stop the iteration.
34+
*
35+
* @param state - An optional state object that can be used to pass additional information to the callback function.
36+
* The state object is cloned for each iteration. This means that changes to the state object are not reflected in the next iteration.
37+
*
38+
* @returns `false` if the iteration was halted, `undefined` otherwise.
39+
*
40+
* @template T - The type of the `state` object.
41+
* @template U - The type of the current node.
42+
*/
43+
forEach<T extends Record<string, unknown>, U extends ContainerNode>(this: U, cb: (entry: {
44+
node: ComponentValue;
45+
parent: ContainerNode;
46+
state?: T;
47+
}, index: number | string) => boolean | void, state?: T): false | undefined;
48+
/**
49+
* Walks the current node and all its children.
50+
*
51+
* @param cb - The callback function to execute for each item.
52+
* The function receives an object containing the current node (`node`), its parent (`parent`),
53+
* and an optional `state` object.
54+
* A second parameter is the index of the current node.
55+
* The function can return `false` to stop the iteration.
56+
*
57+
* @param state - An optional state object that can be used to pass additional information to the callback function.
58+
* The state object is cloned for each iteration. This means that changes to the state object are not reflected in the next iteration.
59+
* However changes are passed down to child node iterations.
60+
*
61+
* @returns `false` if the iteration was halted, `undefined` otherwise.
62+
*
63+
* @template T - The type of the `state` object.
64+
* @template U - The type of the current node.
65+
*/
66+
walk<T extends Record<string, unknown>, U extends ContainerNode>(this: U, cb: (entry: {
67+
node: ComponentValue;
68+
parent: ContainerNode;
69+
state?: T;
70+
}, index: number | string) => boolean | void, state?: T): false | undefined;
71+
}
72+
export declare class FunctionNode extends ContainerNodeBaseClass {
73+
/**
74+
* The node type
75+
* Always `ComponentValueType.Function`
76+
*/
1177
type: ComponentValueType;
78+
/**
79+
* The token for the name of the function.
80+
*/
1281
name: TokenFunction;
82+
/**
83+
* The token for the closing parenthesis of the function.
84+
* If the function is unclosed, this will be an EOF token.
85+
*/
1386
endToken: CSSToken;
14-
value: Array<ComponentValue>;
1587
constructor(name: TokenFunction, endToken: CSSToken, value: Array<ComponentValue>);
88+
/**
89+
* Retrieve the name of the current Function.
90+
* This is the parsed and unescaped name of the function.
91+
*/
1692
getName(): string;
1793
/**
1894
* Normalize the current Function:
1995
* - if the "endToken" is EOF, replace with a ")-token"
2096
*/
2197
normalize(): void;
98+
/**
99+
* Retrieve the tokens for the current Function.
100+
* This is the inverse of parsing from a list of tokens.
101+
*/
22102
tokens(): Array<CSSToken>;
103+
/**
104+
* Convert the current Function to a string.
105+
* This is not a true serialization.
106+
* It is purely a concatenation of the string representation of the tokens.
107+
*/
23108
toString(): string;
24-
indexOf(item: ComponentValue): number | string;
25-
at(index: number | string): ComponentValue | undefined;
26-
walk<T extends Record<string, unknown>>(cb: (entry: {
27-
node: ComponentValue;
28-
parent: ContainerNode;
29-
state?: T;
30-
}, index: number | string) => boolean | void, state?: T): false | undefined;
109+
/**
110+
* A debug helper to convert the current object to a JSON representation.
111+
* This is useful in asserts and to store large ASTs in files.
112+
*/
31113
toJSON(): unknown;
114+
/**
115+
* Check if the current object is a FunctionNode.
116+
* This is a type guard to help with type narrowing.
117+
*/
32118
isFunctionNode(): this is FunctionNode;
119+
/**
120+
* Check if the given object is a FunctionNode.
121+
* This is a type guard to help with type narrowing.
122+
*/
33123
static isFunctionNode(x: unknown): x is FunctionNode;
34124
}
35125
export declare function consumeFunction(ctx: Context, tokens: Array<CSSToken>): {
36126
advance: number;
37127
node: FunctionNode;
38128
};
39-
export declare class SimpleBlockNode {
129+
export declare class SimpleBlockNode extends ContainerNodeBaseClass {
40130
type: ComponentValueType;
41131
startToken: CSSToken;
42132
endToken: CSSToken;
43-
value: Array<ComponentValue>;
44133
constructor(startToken: CSSToken, endToken: CSSToken, value: Array<ComponentValue>);
45134
/**
46135
* Normalize the current Simple Block:
@@ -51,11 +140,6 @@ export declare class SimpleBlockNode {
51140
toString(): string;
52141
indexOf(item: ComponentValue): number | string;
53142
at(index: number | string): ComponentValue | undefined;
54-
walk<T extends Record<string, unknown>>(cb: (entry: {
55-
node: ComponentValue;
56-
parent: ContainerNode;
57-
state?: T;
58-
}, index: number | string) => boolean | void, state?: T): false | undefined;
59143
toJSON(): unknown;
60144
isSimpleBlockNode(): this is SimpleBlockNode;
61145
static isSimpleBlockNode(x: unknown): x is SimpleBlockNode;
@@ -116,3 +200,4 @@ export declare class TokenNode {
116200
isTokenNode(): this is TokenNode;
117201
static isTokenNode(x: unknown): x is TokenNode;
118202
}
203+
export {};

packages/css-parser-algorithms/dist/index.cjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/css-parser-algorithms/dist/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { parseCommaSeparatedListOfComponentValues } from './parse/parse-comma-se
55
export { gatherNodeAncestry } from './util/node-ancestry';
66
export { replaceComponentValues } from './util/replace-component-values';
77
export { stringify } from './util/stringify';
8+
export { walkerIndexGenerator } from './util/walker-index-generator';
89
export { ComponentValueType } from './util/component-value-type';
910
export { isCommentNode, isFunctionNode, isSimpleBlockNode, isTokenNode, isWhitespaceNode, } from './util/type-predicates';
1011
export { sourceIndices } from './util/source-indices';

packages/css-parser-algorithms/dist/index.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Generate a function that finds the next element that should be visited when walking an AST.
3+
* Rules :
4+
* - the previous iteration is used as a reference, so any checks are relative to the start of the current iteration.
5+
* - the next element always appears after the current index.
6+
* - the next element always exists in the list.
7+
* - replacing an element does not cause the replaced element to be visited.
8+
* - removing an element does not cause elements to be skipped.
9+
* - an element added later in the list will be visited.
10+
*/
11+
export declare function walkerIndexGenerator<T>(initialList: Array<T>): (list: Array<T>, child: T, index: number) => number;

0 commit comments

Comments
 (0)