Scope.js
2.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import extractNames from './extractNames.js';
import reserved from '../utils/reserved.js';
import CompileError from '../utils/CompileError.js';
const letConst = /^(?:let|const)$/;
export default function Scope ( options ) {
options = options || {};
this.parent = options.parent;
this.isBlockScope = !!options.block;
let scope = this;
while ( scope.isBlockScope ) scope = scope.parent;
this.functionScope = scope;
this.identifiers = [];
this.declarations = Object.create( null );
this.references = Object.create( null );
this.blockScopedDeclarations = this.isBlockScope ? null : Object.create( null );
this.aliases = this.isBlockScope ? null : Object.create( null );
}
Scope.prototype = {
addDeclaration ( node, kind ) {
for ( const identifier of extractNames( node ) ) {
const name = identifier.name;
const existingDeclaration = this.declarations[ name ];
if ( existingDeclaration && ( letConst.test( kind ) || letConst.test( existingDeclaration.kind ) ) ) {
// TODO warn about double var declarations?
throw new CompileError( identifier, `${name} is already declared` );
}
const declaration = { name, node: identifier, kind, instances: [] };
this.declarations[ name ] = declaration;
if ( this.isBlockScope ) {
if ( !this.functionScope.blockScopedDeclarations[ name ] ) this.functionScope.blockScopedDeclarations[ name ] = [];
this.functionScope.blockScopedDeclarations[ name ].push( declaration );
}
}
},
addReference ( identifier ) {
if ( this.consolidated ) {
this.consolidateReference( identifier );
} else {
this.identifiers.push( identifier );
}
},
consolidate () {
for ( let i = 0; i < this.identifiers.length; i += 1 ) { // we might push to the array during consolidation, so don't cache length
const identifier = this.identifiers[i];
this.consolidateReference( identifier );
}
this.consolidated = true; // TODO understand why this is necessary... seems bad
},
consolidateReference ( identifier ) {
const declaration = this.declarations[ identifier.name ];
if ( declaration ) {
declaration.instances.push( identifier );
} else {
this.references[ identifier.name ] = true;
if ( this.parent ) this.parent.addReference( identifier );
}
},
contains ( name ) {
return this.declarations[ name ] ||
( this.parent ? this.parent.contains( name ) : false );
},
createIdentifier ( base ) {
base = base
.replace( /\s/g, '' )
.replace( /\[([^\]]+)\]/g, '_$1' )
.replace( /[^a-zA-Z0-9_$]/g, '_' )
.replace( /_{2,}/, '_' );
let name = base;
let counter = 1;
while ( this.declarations[ name ] || this.references[ name ] || this.aliases[ name ] || name in reserved ) {
name = `${base}$${counter++}`;
}
this.aliases[ name ] = true;
return name;
},
findDeclaration ( name ) {
return this.declarations[ name ] ||
( this.parent && this.parent.findDeclaration( name ) );
}
};