1 | import { Node, walk } from 'estree-walker';
|
2 | import extractAssignedNames from './extractAssignedNames';
|
3 | import { AttachedScope, AttachScopes } from './pluginutils';
|
4 |
|
5 | const blockDeclarations = {
|
6 | const: true,
|
7 | let: true
|
8 | };
|
9 |
|
10 | interface ScopeOptions {
|
11 | parent?: AttachedScope;
|
12 | block?: boolean;
|
13 | params?: Array<Node>;
|
14 | }
|
15 |
|
16 | class Scope implements AttachedScope {
|
17 | parent?: AttachedScope;
|
18 | isBlockScope: boolean;
|
19 | declarations: { [key: string]: boolean };
|
20 |
|
21 | constructor(options: ScopeOptions = {}) {
|
22 | this.parent = options.parent;
|
23 | this.isBlockScope = !!options.block;
|
24 |
|
25 | this.declarations = Object.create(null);
|
26 |
|
27 | if (options.params) {
|
28 | options.params.forEach(param => {
|
29 | extractAssignedNames(param).forEach(name => {
|
30 | this.declarations[name] = true;
|
31 | });
|
32 | });
|
33 | }
|
34 | }
|
35 |
|
36 | addDeclaration(node: Node, isBlockDeclaration: boolean, isVar: boolean): void {
|
37 | if (!isBlockDeclaration && this.isBlockScope) {
|
38 |
|
39 |
|
40 | this.parent!.addDeclaration(node, isBlockDeclaration, isVar);
|
41 | } else if (node.id) {
|
42 | extractAssignedNames(node.id).forEach(name => {
|
43 | this.declarations[name] = true;
|
44 | });
|
45 | }
|
46 | }
|
47 |
|
48 | contains(name: string): boolean {
|
49 | return this.declarations[name] || (this.parent ? this.parent.contains(name) : false);
|
50 | }
|
51 | }
|
52 |
|
53 | const attachScopes: AttachScopes = function attachScopes(ast, propertyName = 'scope') {
|
54 | let scope = new Scope();
|
55 |
|
56 | walk(ast, {
|
57 | enter(node, parent) {
|
58 |
|
59 |
|
60 | if (/(Function|Class)Declaration/.test(node.type)) {
|
61 | scope.addDeclaration(node, false, false);
|
62 | }
|
63 |
|
64 |
|
65 | if (node.type === 'VariableDeclaration') {
|
66 | const kind: keyof typeof blockDeclarations = node.kind;
|
67 | const isBlockDeclaration = blockDeclarations[kind];
|
68 |
|
69 | node.declarations.forEach((declaration: Node) => {
|
70 | scope.addDeclaration(declaration, isBlockDeclaration, true);
|
71 | });
|
72 | }
|
73 |
|
74 | let newScope: AttachedScope | undefined;
|
75 |
|
76 |
|
77 | if (/Function/.test(node.type)) {
|
78 | newScope = new Scope({
|
79 | parent: scope,
|
80 | block: false,
|
81 | params: node.params
|
82 | });
|
83 |
|
84 |
|
85 |
|
86 | if (node.type === 'FunctionExpression' && node.id) {
|
87 | newScope.addDeclaration(node, false, false);
|
88 | }
|
89 | }
|
90 |
|
91 |
|
92 | if (node.type === 'BlockStatement' && !/Function/.test(parent!.type)) {
|
93 | newScope = new Scope({
|
94 | parent: scope,
|
95 | block: true
|
96 | });
|
97 | }
|
98 |
|
99 |
|
100 | if (node.type === 'CatchClause') {
|
101 | newScope = new Scope({
|
102 | parent: scope,
|
103 | params: node.param ? [node.param] : [],
|
104 | block: true
|
105 | });
|
106 | }
|
107 |
|
108 | if (newScope) {
|
109 | Object.defineProperty(node, propertyName, {
|
110 | value: newScope,
|
111 | configurable: true
|
112 | });
|
113 |
|
114 | scope = newScope;
|
115 | }
|
116 | },
|
117 | leave(node) {
|
118 | if (node[propertyName]) scope = scope.parent!;
|
119 | }
|
120 | });
|
121 |
|
122 | return scope;
|
123 | };
|
124 |
|
125 | export { attachScopes as default };
|