UNPKG

3.04 kBPlain TextView Raw
1import { Node, walk } from 'estree-walker';
2import extractAssignedNames from './extractAssignedNames';
3import { AttachedScope, AttachScopes } from './pluginutils';
4
5const blockDeclarations = {
6 const: true,
7 let: true
8};
9
10interface ScopeOptions {
11 parent?: AttachedScope;
12 block?: boolean;
13 params?: Array<Node>;
14}
15
16class 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 // it's a `var` or function node, and this
39 // is a block scope, so we need to go up
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
53const attachScopes: AttachScopes = function attachScopes(ast, propertyName = 'scope') {
54 let scope = new Scope();
55
56 walk(ast, {
57 enter(node, parent) {
58 // function foo () {...}
59 // class Foo {...}
60 if (/(Function|Class)Declaration/.test(node.type)) {
61 scope.addDeclaration(node, false, false);
62 }
63
64 // var foo = 1
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 // create new function scope
77 if (/Function/.test(node.type)) {
78 newScope = new Scope({
79 parent: scope,
80 block: false,
81 params: node.params
82 });
83
84 // named function expressions - the name is considered
85 // part of the function's scope
86 if (node.type === 'FunctionExpression' && node.id) {
87 newScope.addDeclaration(node, false, false);
88 }
89 }
90
91 // create new block scope
92 if (node.type === 'BlockStatement' && !/Function/.test(parent!.type)) {
93 newScope = new Scope({
94 parent: scope,
95 block: true
96 });
97 }
98
99 // catch clause has its own block scope
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
125export { attachScopes as default };