@aiready/consistency 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +123 -0
- package/dist/chunk-HAOJLJNB.mjs +1290 -0
- package/dist/chunk-IVRBV7SE.mjs +1295 -0
- package/dist/chunk-LD3CHHU2.mjs +1297 -0
- package/dist/chunk-VODCPPET.mjs +1292 -0
- package/dist/chunk-WGH4TGZ3.mjs +1288 -0
- package/dist/cli.js +624 -189
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1196 -182
- package/dist/index.mjs +581 -4
- package/package.json +14 -13
- package/src/analyzer.ts +4 -4
- package/src/analyzers/naming-ast.ts +378 -0
- package/src/index.ts +2 -1
- package/src/utils/ast-parser.ts +181 -0
- package/src/utils/context-detector.ts +278 -0
- package/src/utils/scope-tracker.ts +221 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
|
2
|
+
import { traverseAST } from './ast-parser';
|
|
3
|
+
|
|
4
|
+
export type FileType = 'test' | 'production' | 'config' | 'types';
|
|
5
|
+
export type CodeLayer = 'api' | 'business' | 'data' | 'utility' | 'unknown';
|
|
6
|
+
|
|
7
|
+
export interface CodeContext {
|
|
8
|
+
fileType: FileType;
|
|
9
|
+
codeLayer: CodeLayer;
|
|
10
|
+
complexity: number;
|
|
11
|
+
isTestFile: boolean;
|
|
12
|
+
isTypeDefinition: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect the file type based on file path and content
|
|
17
|
+
*/
|
|
18
|
+
export function detectFileType(filePath: string, ast: TSESTree.Program): FileType {
|
|
19
|
+
const path = filePath.toLowerCase();
|
|
20
|
+
|
|
21
|
+
// Test files
|
|
22
|
+
if (path.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/) || path.includes('__tests__')) {
|
|
23
|
+
return 'test';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Type definition files
|
|
27
|
+
if (path.endsWith('.d.ts') || path.includes('types')) {
|
|
28
|
+
return 'types';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Config files
|
|
32
|
+
if (path.match(/config|\.config\.|rc\.|setup/) || path.includes('configuration')) {
|
|
33
|
+
return 'config';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return 'production';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect the code layer based on imports and exports
|
|
41
|
+
*/
|
|
42
|
+
export function detectCodeLayer(ast: TSESTree.Program): CodeLayer {
|
|
43
|
+
let hasAPIIndicators = 0;
|
|
44
|
+
let hasBusinessIndicators = 0;
|
|
45
|
+
let hasDataIndicators = 0;
|
|
46
|
+
let hasUtilityIndicators = 0;
|
|
47
|
+
|
|
48
|
+
traverseAST(ast, {
|
|
49
|
+
enter: (node) => {
|
|
50
|
+
// Check imports
|
|
51
|
+
if (node.type === 'ImportDeclaration') {
|
|
52
|
+
const source = node.source.value as string;
|
|
53
|
+
|
|
54
|
+
if (source.match(/express|fastify|koa|@nestjs|axios|fetch|http/i)) {
|
|
55
|
+
hasAPIIndicators++;
|
|
56
|
+
}
|
|
57
|
+
if (source.match(/database|prisma|typeorm|sequelize|mongoose|pg|mysql/i)) {
|
|
58
|
+
hasDataIndicators++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check function names for layer indicators
|
|
63
|
+
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
64
|
+
const name = node.id.name;
|
|
65
|
+
|
|
66
|
+
// API layer patterns
|
|
67
|
+
if (name.match(/^(get|post|put|delete|patch|handle|api|route|controller)/i)) {
|
|
68
|
+
hasAPIIndicators++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Business logic patterns
|
|
72
|
+
if (name.match(/^(calculate|process|validate|transform|compute|analyze)/i)) {
|
|
73
|
+
hasBusinessIndicators++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Data layer patterns
|
|
77
|
+
if (name.match(/^(find|create|update|delete|save|fetch|query|insert)/i)) {
|
|
78
|
+
hasDataIndicators++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Utility patterns
|
|
82
|
+
if (name.match(/^(format|parse|convert|normalize|sanitize|encode|decode)/i)) {
|
|
83
|
+
hasUtilityIndicators++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for exports
|
|
88
|
+
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
|
89
|
+
// Functions exported with "api", "handler", "route" suggest API layer
|
|
90
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
91
|
+
if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
|
|
92
|
+
const name = node.declaration.id.name;
|
|
93
|
+
if (name.match(/handler|route|api|controller/i)) {
|
|
94
|
+
hasAPIIndicators += 2; // Stronger signal
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Determine layer based on indicators
|
|
103
|
+
const scores = {
|
|
104
|
+
api: hasAPIIndicators,
|
|
105
|
+
business: hasBusinessIndicators,
|
|
106
|
+
data: hasDataIndicators,
|
|
107
|
+
utility: hasUtilityIndicators,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const maxScore = Math.max(...Object.values(scores));
|
|
111
|
+
if (maxScore === 0) {
|
|
112
|
+
return 'unknown';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Return the layer with highest score
|
|
116
|
+
if (scores.api === maxScore) return 'api';
|
|
117
|
+
if (scores.data === maxScore) return 'data';
|
|
118
|
+
if (scores.business === maxScore) return 'business';
|
|
119
|
+
if (scores.utility === maxScore) return 'utility';
|
|
120
|
+
|
|
121
|
+
return 'unknown';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Calculate cyclomatic complexity for a function
|
|
126
|
+
*/
|
|
127
|
+
export function calculateComplexity(node: TSESTree.Node): number {
|
|
128
|
+
let complexity = 1; // Base complexity
|
|
129
|
+
|
|
130
|
+
traverseAST(node, {
|
|
131
|
+
enter: (childNode) => {
|
|
132
|
+
// Each decision point adds 1 to complexity
|
|
133
|
+
switch (childNode.type) {
|
|
134
|
+
case 'IfStatement':
|
|
135
|
+
case 'ConditionalExpression': // ternary
|
|
136
|
+
case 'SwitchCase':
|
|
137
|
+
case 'ForStatement':
|
|
138
|
+
case 'ForInStatement':
|
|
139
|
+
case 'ForOfStatement':
|
|
140
|
+
case 'WhileStatement':
|
|
141
|
+
case 'DoWhileStatement':
|
|
142
|
+
case 'CatchClause':
|
|
143
|
+
complexity++;
|
|
144
|
+
break;
|
|
145
|
+
case 'LogicalExpression':
|
|
146
|
+
// && and || add complexity
|
|
147
|
+
if (childNode.operator === '&&' || childNode.operator === '||') {
|
|
148
|
+
complexity++;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return complexity;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a complete context for a file
|
|
160
|
+
*/
|
|
161
|
+
export function buildCodeContext(filePath: string, ast: TSESTree.Program): CodeContext {
|
|
162
|
+
const fileType = detectFileType(filePath, ast);
|
|
163
|
+
const codeLayer = detectCodeLayer(ast);
|
|
164
|
+
|
|
165
|
+
// Calculate average complexity of functions in file
|
|
166
|
+
let totalComplexity = 0;
|
|
167
|
+
let functionCount = 0;
|
|
168
|
+
|
|
169
|
+
traverseAST(ast, {
|
|
170
|
+
enter: (node) => {
|
|
171
|
+
if (
|
|
172
|
+
node.type === 'FunctionDeclaration' ||
|
|
173
|
+
node.type === 'FunctionExpression' ||
|
|
174
|
+
node.type === 'ArrowFunctionExpression'
|
|
175
|
+
) {
|
|
176
|
+
totalComplexity += calculateComplexity(node);
|
|
177
|
+
functionCount++;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const avgComplexity = functionCount > 0 ? totalComplexity / functionCount : 1;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
fileType,
|
|
186
|
+
codeLayer,
|
|
187
|
+
complexity: Math.round(avgComplexity),
|
|
188
|
+
isTestFile: fileType === 'test',
|
|
189
|
+
isTypeDefinition: fileType === 'types',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get context-adjusted severity based on code context
|
|
195
|
+
*/
|
|
196
|
+
export function adjustSeverity(
|
|
197
|
+
baseSeverity: 'info' | 'minor' | 'major' | 'critical',
|
|
198
|
+
context: CodeContext,
|
|
199
|
+
issueType: string
|
|
200
|
+
): 'info' | 'minor' | 'major' | 'critical' {
|
|
201
|
+
// Test files: Be more lenient
|
|
202
|
+
if (context.isTestFile) {
|
|
203
|
+
if (baseSeverity === 'minor') return 'info';
|
|
204
|
+
if (baseSeverity === 'major') return 'minor';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Type definition files: Be more lenient (often use short generic names)
|
|
208
|
+
if (context.isTypeDefinition) {
|
|
209
|
+
if (baseSeverity === 'minor') return 'info';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// API layer: Be stricter (public interface)
|
|
213
|
+
if (context.codeLayer === 'api') {
|
|
214
|
+
if (baseSeverity === 'info' && issueType === 'unclear') return 'minor';
|
|
215
|
+
if (baseSeverity === 'minor' && issueType === 'unclear') return 'major';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// High complexity: Be stricter (need clearer names)
|
|
219
|
+
if (context.complexity > 10) {
|
|
220
|
+
if (baseSeverity === 'info') return 'minor';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Utility/helper layer: Allow shorter names
|
|
224
|
+
if (context.codeLayer === 'utility') {
|
|
225
|
+
if (baseSeverity === 'minor' && issueType === 'abbreviation') return 'info';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return baseSeverity;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if a short variable name is acceptable in this context
|
|
233
|
+
*/
|
|
234
|
+
export function isAcceptableInContext(
|
|
235
|
+
name: string,
|
|
236
|
+
context: CodeContext,
|
|
237
|
+
options: {
|
|
238
|
+
isLoopVariable?: boolean;
|
|
239
|
+
isParameter?: boolean;
|
|
240
|
+
isDestructured?: boolean;
|
|
241
|
+
complexity?: number;
|
|
242
|
+
}
|
|
243
|
+
): boolean {
|
|
244
|
+
// Loop variables always acceptable
|
|
245
|
+
if (options.isLoopVariable && ['i', 'j', 'k', 'l', 'n', 'm'].includes(name)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Test files: More lenient
|
|
250
|
+
if (context.isTestFile) {
|
|
251
|
+
// Common test patterns: a/b for comparison, x/y for coordinates
|
|
252
|
+
if (['a', 'b', 'c', 'x', 'y', 'z'].includes(name) && options.isParameter) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Math/graphics context: x, y, z acceptable
|
|
258
|
+
if (context.codeLayer === 'utility' && ['x', 'y', 'z'].includes(name)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Destructured from well-named source: More lenient
|
|
263
|
+
if (options.isDestructured) {
|
|
264
|
+
// Coverage metrics s/b/f/l always acceptable when destructured
|
|
265
|
+
if (['s', 'b', 'f', 'l'].includes(name)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Simple functions (complexity < 3): Allow short parameter names
|
|
271
|
+
if (options.isParameter && (options.complexity ?? context.complexity) < 3) {
|
|
272
|
+
if (name.length >= 2) {
|
|
273
|
+
return true; // Two-letter names OK in simple functions
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
|
2
|
+
|
|
3
|
+
export type ScopeType = 'global' | 'function' | 'block' | 'loop' | 'class';
|
|
4
|
+
|
|
5
|
+
export interface VariableInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
node: TSESTree.Node;
|
|
8
|
+
declarationLine: number;
|
|
9
|
+
references: TSESTree.Node[];
|
|
10
|
+
type?: TSESTree.TypeNode | null;
|
|
11
|
+
isParameter: boolean;
|
|
12
|
+
isDestructured: boolean;
|
|
13
|
+
isLoopVariable: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Scope {
|
|
17
|
+
type: ScopeType;
|
|
18
|
+
node: TSESTree.Node;
|
|
19
|
+
parent: Scope | null;
|
|
20
|
+
children: Scope[];
|
|
21
|
+
variables: Map<string, VariableInfo>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ScopeTracker {
|
|
25
|
+
private currentScope: Scope;
|
|
26
|
+
private readonly rootScope: Scope;
|
|
27
|
+
private readonly allScopes: Scope[] = [];
|
|
28
|
+
|
|
29
|
+
constructor(rootNode: TSESTree.Program) {
|
|
30
|
+
this.rootScope = {
|
|
31
|
+
type: 'global',
|
|
32
|
+
node: rootNode,
|
|
33
|
+
parent: null,
|
|
34
|
+
children: [],
|
|
35
|
+
variables: new Map(),
|
|
36
|
+
};
|
|
37
|
+
this.currentScope = this.rootScope;
|
|
38
|
+
this.allScopes.push(this.rootScope);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enter a new scope
|
|
43
|
+
*/
|
|
44
|
+
enterScope(type: ScopeType, node: TSESTree.Node): void {
|
|
45
|
+
const newScope: Scope = {
|
|
46
|
+
type,
|
|
47
|
+
node,
|
|
48
|
+
parent: this.currentScope,
|
|
49
|
+
children: [],
|
|
50
|
+
variables: new Map(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.currentScope.children.push(newScope);
|
|
54
|
+
this.currentScope = newScope;
|
|
55
|
+
this.allScopes.push(newScope);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Exit current scope and return to parent
|
|
60
|
+
*/
|
|
61
|
+
exitScope(): void {
|
|
62
|
+
if (this.currentScope.parent) {
|
|
63
|
+
this.currentScope = this.currentScope.parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Declare a variable in the current scope
|
|
69
|
+
*/
|
|
70
|
+
declareVariable(
|
|
71
|
+
name: string,
|
|
72
|
+
node: TSESTree.Node,
|
|
73
|
+
line: number,
|
|
74
|
+
options: {
|
|
75
|
+
type?: TSESTree.TypeNode | null;
|
|
76
|
+
isParameter?: boolean;
|
|
77
|
+
isDestructured?: boolean;
|
|
78
|
+
isLoopVariable?: boolean;
|
|
79
|
+
} = {}
|
|
80
|
+
): void {
|
|
81
|
+
const varInfo: VariableInfo = {
|
|
82
|
+
name,
|
|
83
|
+
node,
|
|
84
|
+
declarationLine: line,
|
|
85
|
+
references: [],
|
|
86
|
+
type: options.type,
|
|
87
|
+
isParameter: options.isParameter ?? false,
|
|
88
|
+
isDestructured: options.isDestructured ?? false,
|
|
89
|
+
isLoopVariable: options.isLoopVariable ?? false,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.currentScope.variables.set(name, varInfo);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add a reference to a variable
|
|
97
|
+
*/
|
|
98
|
+
addReference(name: string, node: TSESTree.Node): void {
|
|
99
|
+
const varInfo = this.findVariable(name);
|
|
100
|
+
if (varInfo) {
|
|
101
|
+
varInfo.references.push(node);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find a variable in current or parent scopes
|
|
107
|
+
*/
|
|
108
|
+
findVariable(name: string): VariableInfo | null {
|
|
109
|
+
let scope: Scope | null = this.currentScope;
|
|
110
|
+
|
|
111
|
+
while (scope) {
|
|
112
|
+
const varInfo = scope.variables.get(name);
|
|
113
|
+
if (varInfo) {
|
|
114
|
+
return varInfo;
|
|
115
|
+
}
|
|
116
|
+
scope = scope.parent;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all variables in current scope (not including parent scopes)
|
|
124
|
+
*/
|
|
125
|
+
getCurrentScopeVariables(): VariableInfo[] {
|
|
126
|
+
return Array.from(this.currentScope.variables.values());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all variables across all scopes
|
|
131
|
+
*/
|
|
132
|
+
getAllVariables(): VariableInfo[] {
|
|
133
|
+
const allVars: VariableInfo[] = [];
|
|
134
|
+
|
|
135
|
+
for (const scope of this.allScopes) {
|
|
136
|
+
allVars.push(...Array.from(scope.variables.values()));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return allVars;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculate actual usage count (references minus declaration)
|
|
144
|
+
*/
|
|
145
|
+
getUsageCount(varInfo: VariableInfo): number {
|
|
146
|
+
return varInfo.references.length;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a variable is short-lived (used within N lines)
|
|
151
|
+
*/
|
|
152
|
+
isShortLived(varInfo: VariableInfo, maxLines: number = 5): boolean {
|
|
153
|
+
if (varInfo.references.length === 0) {
|
|
154
|
+
return false; // Unused variable
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const declarationLine = varInfo.declarationLine;
|
|
158
|
+
const maxUsageLine = Math.max(
|
|
159
|
+
...varInfo.references.map(ref => ref.loc?.start.line ?? declarationLine)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (maxUsageLine - declarationLine) <= maxLines;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a variable is used in a limited scope (e.g., only in one callback)
|
|
167
|
+
*/
|
|
168
|
+
isLocallyScoped(varInfo: VariableInfo): boolean {
|
|
169
|
+
// If all references are within a small scope (e.g., arrow function), it's locally scoped
|
|
170
|
+
if (varInfo.references.length === 0) return false;
|
|
171
|
+
|
|
172
|
+
// Check if usage span is small
|
|
173
|
+
const lines = varInfo.references.map(ref => ref.loc?.start.line ?? 0);
|
|
174
|
+
const minLine = Math.min(...lines);
|
|
175
|
+
const maxLine = Math.max(...lines);
|
|
176
|
+
|
|
177
|
+
return (maxLine - minLine) <= 3;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get current scope type
|
|
182
|
+
*/
|
|
183
|
+
getCurrentScopeType(): ScopeType {
|
|
184
|
+
return this.currentScope.type;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if currently in a loop scope
|
|
189
|
+
*/
|
|
190
|
+
isInLoop(): boolean {
|
|
191
|
+
let scope: Scope | null = this.currentScope;
|
|
192
|
+
while (scope) {
|
|
193
|
+
if (scope.type === 'loop') {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
scope = scope.parent;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if currently in a function scope
|
|
203
|
+
*/
|
|
204
|
+
isInFunction(): boolean {
|
|
205
|
+
let scope: Scope | null = this.currentScope;
|
|
206
|
+
while (scope) {
|
|
207
|
+
if (scope.type === 'function') {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
scope = scope.parent;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get the root scope
|
|
217
|
+
*/
|
|
218
|
+
getRootScope(): Scope {
|
|
219
|
+
return this.rootScope;
|
|
220
|
+
}
|
|
221
|
+
}
|