Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6eb97d4

Browse files
authored
feat(eslint-plugin): (EXPERIMENTAL) begin indent rewrite (typescript-eslint#439)
1 parent 4e193ca commit 6eb97d4

File tree

19 files changed

+13038
-21
lines changed

19 files changed

+13038
-21
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"rules": {
1010
"comma-dangle": ["error", "always-multiline"],
1111
"curly": ["error", "all"],
12+
"no-dupe-class-members": "off",
1213
"no-mixed-operators": "error",
1314
"no-console": "off",
1415
"no-undef": "off",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// The following code is adapted from the the code in eslint.
2+
// License: https://github.com/eslint/eslint/blob/48700fc8408f394887cdedd071b22b757700fdcb/LICENSE
3+
4+
import { TSESTree } from '@typescript-eslint/typescript-estree';
5+
import createTree = require('functional-red-black-tree');
6+
7+
export type TokenOrComment = TSESTree.Token | TSESTree.Comment;
8+
export interface TreeValue {
9+
offset: number;
10+
from: TokenOrComment | null;
11+
force: boolean;
12+
}
13+
14+
/**
15+
* A mutable balanced binary search tree that stores (key, value) pairs. The keys are numeric, and must be unique.
16+
* This is intended to be a generic wrapper around a balanced binary search tree library, so that the underlying implementation
17+
* can easily be swapped out.
18+
*/
19+
export class BinarySearchTree {
20+
private rbTree = createTree<TreeValue, number>();
21+
22+
/**
23+
* Inserts an entry into the tree.
24+
*/
25+
public insert(key: number, value: TreeValue): void {
26+
const iterator = this.rbTree.find(key);
27+
28+
if (iterator.valid) {
29+
this.rbTree = iterator.update(value);
30+
} else {
31+
this.rbTree = this.rbTree.insert(key, value);
32+
}
33+
}
34+
35+
/**
36+
* Finds the entry with the largest key less than or equal to the provided key
37+
* @returns The found entry, or null if no such entry exists.
38+
*/
39+
public findLe(key: number): { key: number; value: TreeValue } {
40+
const iterator = this.rbTree.le(key);
41+
42+
return { key: iterator.key, value: iterator.value };
43+
}
44+
45+
/**
46+
* Deletes all of the keys in the interval [start, end)
47+
*/
48+
public deleteRange(start: number, end: number): void {
49+
// Exit without traversing the tree if the range has zero size.
50+
if (start === end) {
51+
return;
52+
}
53+
const iterator = this.rbTree.ge(start);
54+
55+
while (iterator.valid && iterator.key < end) {
56+
this.rbTree = this.rbTree.remove(iterator.key);
57+
iterator.next();
58+
}
59+
}
60+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// The following code is adapted from the the code in eslint.
2+
// License: https://github.com/eslint/eslint/blob/48700fc8408f394887cdedd071b22b757700fdcb/LICENSE
3+
4+
import { TokenInfo } from './TokenInfo';
5+
import { BinarySearchTree, TokenOrComment } from './BinarySearchTree';
6+
import { TSESTree } from '@typescript-eslint/typescript-estree';
7+
8+
/**
9+
* A class to store information on desired offsets of tokens from each other
10+
*/
11+
export class OffsetStorage {
12+
private tokenInfo: TokenInfo;
13+
private indentSize: number;
14+
private indentType: string;
15+
private tree: BinarySearchTree;
16+
private lockedFirstTokens: WeakMap<TokenOrComment, TokenOrComment>;
17+
private desiredIndentCache: WeakMap<TokenOrComment, string>;
18+
private ignoredTokens: WeakSet<TokenOrComment>;
19+
/**
20+
* @param tokenInfo a TokenInfo instance
21+
* @param indentSize The desired size of each indentation level
22+
* @param indentType The indentation character
23+
*/
24+
constructor(tokenInfo: TokenInfo, indentSize: number, indentType: string) {
25+
this.tokenInfo = tokenInfo;
26+
this.indentSize = indentSize;
27+
this.indentType = indentType;
28+
29+
this.tree = new BinarySearchTree();
30+
this.tree.insert(0, { offset: 0, from: null, force: false });
31+
32+
this.lockedFirstTokens = new WeakMap();
33+
this.desiredIndentCache = new WeakMap();
34+
this.ignoredTokens = new WeakSet();
35+
}
36+
37+
private getOffsetDescriptor(token: TokenOrComment) {
38+
return this.tree.findLe(token.range[0]).value;
39+
}
40+
41+
/**
42+
* Sets the offset column of token B to match the offset column of token A.
43+
* **WARNING**: This matches a *column*, even if baseToken is not the first token on its line. In
44+
* most cases, `setDesiredOffset` should be used instead.
45+
* @param baseToken The first token
46+
* @param offsetToken The second token, whose offset should be matched to the first token
47+
*/
48+
public matchOffsetOf(
49+
baseToken: TokenOrComment,
50+
offsetToken: TokenOrComment,
51+
): void {
52+
/*
53+
* lockedFirstTokens is a map from a token whose indentation is controlled by the "first" option to
54+
* the token that it depends on. For example, with the `ArrayExpression: first` option, the first
55+
* token of each element in the array after the first will be mapped to the first token of the first
56+
* element. The desired indentation of each of these tokens is computed based on the desired indentation
57+
* of the "first" element, rather than through the normal offset mechanism.
58+
*/
59+
this.lockedFirstTokens.set(offsetToken, baseToken);
60+
}
61+
62+
/**
63+
* Sets the desired offset of a token.
64+
*
65+
* This uses a line-based offset collapsing behavior to handle tokens on the same line.
66+
* For example, consider the following two cases:
67+
*
68+
* (
69+
* [
70+
* bar
71+
* ]
72+
* )
73+
*
74+
* ([
75+
* bar
76+
* ])
77+
*
78+
* Based on the first case, it's clear that the `bar` token needs to have an offset of 1 indent level (4 spaces) from
79+
* the `[` token, and the `[` token has to have an offset of 1 indent level from the `(` token. Since the `(` token is
80+
* the first on its line (with an indent of 0 spaces), the `bar` token needs to be offset by 2 indent levels (8 spaces)
81+
* from the start of its line.
82+
*
83+
* However, in the second case `bar` should only be indented by 4 spaces. This is because the offset of 1 indent level
84+
* between the `(` and the `[` tokens gets "collapsed" because the two tokens are on the same line. As a result, the
85+
* `(` token is mapped to the `[` token with an offset of 0, and the rule correctly decides that `bar` should be indented
86+
* by 1 indent level from the start of the line.
87+
*
88+
* This is useful because rule listeners can usually just call `setDesiredOffset` for all the tokens in the node,
89+
* without needing to check which lines those tokens are on.
90+
*
91+
* Note that since collapsing only occurs when two tokens are on the same line, there are a few cases where non-intuitive
92+
* behavior can occur. For example, consider the following cases:
93+
*
94+
* foo(
95+
* ).
96+
* bar(
97+
* baz
98+
* )
99+
*
100+
* foo(
101+
* ).bar(
102+
* baz
103+
* )
104+
*
105+
* Based on the first example, it would seem that `bar` should be offset by 1 indent level from `foo`, and `baz`
106+
* should be offset by 1 indent level from `bar`. However, this is not correct, because it would result in `baz`
107+
* being indented by 2 indent levels in the second case (since `foo`, `bar`, and `baz` are all on separate lines, no
108+
* collapsing would occur).
109+
*
110+
* Instead, the correct way would be to offset `baz` by 1 level from `bar`, offset `bar` by 1 level from the `)`, and
111+
* offset the `)` by 0 levels from `foo`. This ensures that the offset between `bar` and the `)` are correctly collapsed
112+
* in the second case.
113+
*
114+
* @param token The token
115+
* @param fromToken The token that `token` should be offset from
116+
* @param offset The desired indent level
117+
*/
118+
public setDesiredOffset(
119+
token: TokenOrComment,
120+
fromToken: TokenOrComment | null,
121+
offset: number,
122+
): void {
123+
this.setDesiredOffsets(token.range, fromToken, offset);
124+
}
125+
126+
/**
127+
* Sets the desired offset of all tokens in a range
128+
* It's common for node listeners in this file to need to apply the same offset to a large, contiguous range of tokens.
129+
* Moreover, the offset of any given token is usually updated multiple times (roughly once for each node that contains
130+
* it). This means that the offset of each token is updated O(AST depth) times.
131+
* It would not be performant to store and update the offsets for each token independently, because the rule would end
132+
* up having a time complexity of O(number of tokens * AST depth), which is quite slow for large files.
133+
*
134+
* Instead, the offset tree is represented as a collection of contiguous offset ranges in a file. For example, the following
135+
* list could represent the state of the offset tree at a given point:
136+
*
137+
* * Tokens starting in the interval [0, 15) are aligned with the beginning of the file
138+
* * Tokens starting in the interval [15, 30) are offset by 1 indent level from the `bar` token
139+
* * Tokens starting in the interval [30, 43) are offset by 1 indent level from the `foo` token
140+
* * Tokens starting in the interval [43, 820) are offset by 2 indent levels from the `bar` token
141+
* * Tokens starting in the interval [820, ∞) are offset by 1 indent level from the `baz` token
142+
*
143+
* The `setDesiredOffsets` methods inserts ranges like the ones above. The third line above would be inserted by using:
144+
* `setDesiredOffsets([30, 43], fooToken, 1);`
145+
*
146+
* @param range A [start, end] pair. All tokens with range[0] <= token.start < range[1] will have the offset applied.
147+
* @param fromToken The token that this is offset from
148+
* @param offset The desired indent level
149+
* @param force `true` if this offset should not use the normal collapsing behavior. This should almost always be false.
150+
*/
151+
public setDesiredOffsets(
152+
range: [number, number],
153+
fromToken: TokenOrComment | null,
154+
offset: number = 0,
155+
force: boolean = false,
156+
): void {
157+
/*
158+
* Offset ranges are stored as a collection of nodes, where each node maps a numeric key to an offset
159+
* descriptor. The tree for the example above would have the following nodes:
160+
*
161+
* * key: 0, value: { offset: 0, from: null }
162+
* * key: 15, value: { offset: 1, from: barToken }
163+
* * key: 30, value: { offset: 1, from: fooToken }
164+
* * key: 43, value: { offset: 2, from: barToken }
165+
* * key: 820, value: { offset: 1, from: bazToken }
166+
*
167+
* To find the offset descriptor for any given token, one needs to find the node with the largest key
168+
* which is <= token.start. To make this operation fast, the nodes are stored in a balanced binary
169+
* search tree indexed by key.
170+
*/
171+
172+
const descriptorToInsert = { offset, from: fromToken, force };
173+
174+
const descriptorAfterRange = this.tree.findLe(range[1]).value;
175+
176+
const fromTokenIsInRange =
177+
fromToken &&
178+
fromToken.range[0] >= range[0] &&
179+
fromToken.range[1] <= range[1];
180+
// this has to be before the delete + insert below or else you'll get into a cycle
181+
const fromTokenDescriptor = fromTokenIsInRange
182+
? this.getOffsetDescriptor(fromToken!)
183+
: null;
184+
185+
// First, remove any existing nodes in the range from the tree.
186+
this.tree.deleteRange(range[0] + 1, range[1]);
187+
188+
// Insert a new node into the tree for this range
189+
this.tree.insert(range[0], descriptorToInsert);
190+
191+
/*
192+
* To avoid circular offset dependencies, keep the `fromToken` token mapped to whatever it was mapped to previously,
193+
* even if it's in the current range.
194+
*/
195+
if (fromTokenIsInRange) {
196+
this.tree.insert(fromToken!.range[0], fromTokenDescriptor!);
197+
this.tree.insert(fromToken!.range[1], descriptorToInsert);
198+
}
199+
200+
/*
201+
* To avoid modifying the offset of tokens after the range, insert another node to keep the offset of the following
202+
* tokens the same as it was before.
203+
*/
204+
this.tree.insert(range[1], descriptorAfterRange);
205+
}
206+
207+
/**
208+
* Gets the desired indent of a token
209+
* @returns The desired indent of the token
210+
*/
211+
public getDesiredIndent(token: TokenOrComment): string {
212+
if (!this.desiredIndentCache.has(token)) {
213+
if (this.ignoredTokens.has(token)) {
214+
/*
215+
* If the token is ignored, use the actual indent of the token as the desired indent.
216+
* This ensures that no errors are reported for this token.
217+
*/
218+
this.desiredIndentCache.set(
219+
token,
220+
this.tokenInfo.getTokenIndent(token),
221+
);
222+
} else if (this.lockedFirstTokens.has(token)) {
223+
const firstToken = this.lockedFirstTokens.get(token)!;
224+
225+
this.desiredIndentCache.set(
226+
token,
227+
228+
// (indentation for the first element's line)
229+
this.getDesiredIndent(
230+
this.tokenInfo.getFirstTokenOfLine(firstToken),
231+
) +
232+
// (space between the start of the first element's line and the first element)
233+
this.indentType.repeat(
234+
firstToken.loc.start.column -
235+
this.tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column,
236+
),
237+
);
238+
} else {
239+
const offsetInfo = this.getOffsetDescriptor(token);
240+
const offset =
241+
offsetInfo.from &&
242+
offsetInfo.from.loc.start.line === token.loc.start.line &&
243+
!/^\s*?\n/u.test(token.value) &&
244+
!offsetInfo.force
245+
? 0
246+
: offsetInfo.offset * this.indentSize;
247+
248+
this.desiredIndentCache.set(
249+
token,
250+
(offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : '') +
251+
this.indentType.repeat(offset),
252+
);
253+
}
254+
}
255+
256+
return this.desiredIndentCache.get(token)!;
257+
}
258+
259+
/**
260+
* Ignores a token, preventing it from being reported.
261+
*/
262+
ignoreToken(token: TokenOrComment): void {
263+
if (this.tokenInfo.isFirstTokenOfLine(token)) {
264+
this.ignoredTokens.add(token);
265+
}
266+
}
267+
268+
/**
269+
* Gets the first token that the given token's indentation is dependent on
270+
* @returns The token that the given token depends on, or `null` if the given token is at the top level
271+
*/
272+
getFirstDependency(token: TSESTree.Token): TSESTree.Token | null;
273+
getFirstDependency(token: TokenOrComment): TokenOrComment | null;
274+
getFirstDependency(token: TokenOrComment): TokenOrComment | null {
275+
return this.getOffsetDescriptor(token).from;
276+
}
277+
}

0 commit comments

Comments
 (0)