pkg-scaffold 2.4.0 → 3.1.0

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.
@@ -1,313 +1,266 @@
1
1
  import ts from 'typescript';
2
2
  import fs from 'fs/promises';
3
- import crypto from 'crypto';
3
+ import path from 'path';
4
4
 
5
5
  /**
6
6
  * Enterprise AST Syntax Walker & Feature Extractor
7
- * Utilizes the official TypeScript Compiler infrastructure to execute deeply nested
8
- * node classification without falling back to high-risk regular expression approximations.
7
+ * Upgraded to use full TypeScript Compiler API (ts.createProgram + TypeChecker)
8
+ * for type-aware cross-file analysis.
9
9
  */
10
+ import { OxcAnalyzer } from './OxcAnalyzer.js';
11
+
10
12
  export class ASTAnalyzer {
11
13
  constructor(context) {
12
14
  this.context = context;
13
- // Standard high-entropy baseline selectors for AST variable tracking
14
- this.entropyThreshold = 4.3;
15
+ this.program = null;
16
+ this.checker = null;
17
+ this.oxc = new OxcAnalyzer(context);
15
18
  }
16
19
 
17
20
  /**
18
- * Parses target file data into an isolated AST representation and populates metadata structures.
19
- * @param {string} filePath - Absolute path to on-disk component
20
- * @param {Object} fileNode - In-memory structural graph reference node
21
+ * Initializes the TypeScript program for the entire project.
22
+ * This is crucial for cross-file type resolution.
23
+ */
24
+ initProgram(filePaths, options = {}) {
25
+ const defaultOptions = {
26
+ target: ts.ScriptTarget.Latest,
27
+ module: ts.ModuleKind.CommonJS,
28
+ allowJs: true,
29
+ checkJs: true,
30
+ esModuleInterop: true,
31
+ skipLibCheck: true
32
+ };
33
+
34
+ this.program = ts.createProgram(filePaths, { ...defaultOptions, ...options });
35
+ this.checker = this.program.getTypeChecker();
36
+ }
37
+
38
+ /**
39
+ * Processes a file using the initialized program and type checker.
21
40
  */
22
41
  async processFile(filePath, fileNode) {
23
- try {
24
- const sourceText = await fs.readFile(filePath, 'utf8');
25
-
26
- // Configure target extraction structures to parse TS, JSX, and modern TC39 specifications
27
- const sourceFile = ts.createSourceFile(
28
- filePath,
29
- sourceText,
30
- ts.ScriptTarget.Latest,
31
- true, // Ensure parent pointers are bound to allow localized subtree walking
32
- this.getScriptKind(filePath)
33
- );
42
+ // Fast Path: Use OXC for rapid scanning if type checking is not strictly required for this file
43
+ if (this.context.fastMode) {
44
+ return await this.oxc.processFile(filePath, fileNode);
45
+ }
34
46
 
35
- this.walkNode(sourceFile, sourceFile, fileNode);
36
- this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
37
-
38
- return true;
39
- } catch (parseError) {
47
+ if (!this.program) {
48
+ throw new Error('ASTAnalyzer must be initialized with initProgram() before processing files.');
49
+ }
50
+
51
+ const sourceFile = this.program.getSourceFile(filePath);
52
+ if (!sourceFile) {
40
53
  if (this.context.verbose) {
41
- console.error(`[AST Open Error] Failed compilation validation mapping on element: ${filePath}. Reason: ${parseError.message}`);
54
+ console.error(`[AST Error] Source file not found in program: ${filePath}`);
42
55
  }
43
56
  return false;
44
57
  }
58
+
59
+ this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
60
+ this.walkNode(sourceFile, sourceFile, fileNode);
61
+
62
+ return true;
45
63
  }
46
64
 
47
- /**
48
- * Primary node walker loop executing atomic switch classifications.
49
- * Challenge #7: Resolves conditional/destructured references to prevent cascading breakages.
50
- */
51
65
  walkNode(sourceFile, node, fileNode) {
52
66
  if (!node) return;
53
67
 
54
- switch (node.kind) {
55
- // Handle Explicit Named or Absolute Star Namespace Imports
56
- case ts.SyntaxKind.ImportDeclaration: {
57
- if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
58
- const specifier = node.moduleSpecifier.text;
59
- fileNode.explicitImports.add(specifier);
60
-
61
- if (node.importClause) {
62
- // Trace named bounds: import { activeToken } from 'module';
63
- if (node.importClause.namedBindings) {
64
- if (ts.isNamedImports(node.importClause.namedBindings)) {
65
- node.importClause.namedBindings.elements.forEach(element => {
66
- const importedName = element.name.text;
67
- const propertyName = element.propertyName ? element.propertyName.text : importedName;
68
- fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
69
- });
70
- } else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
71
- // Tracking total wildcard imports: import * as layout from 'module';
72
- fileNode.importedSymbols.add(`${specifier}:*`);
73
- }
74
- }
75
- // Trace default bounds: import React from 'react';
76
- if (node.importClause.name) {
77
- fileNode.importedSymbols.add(`${specifier}:default`);
78
- }
68
+ // Use type checker to resolve symbols if needed
69
+ if (ts.isIdentifier(node) && !this.isNodeDeclarationName(node)) {
70
+ const symbol = this.checker.getSymbolAtLocation(node);
71
+ if (symbol) {
72
+ const declarations = symbol.getDeclarations();
73
+ if (declarations && declarations.length > 0) {
74
+ const declFile = declarations[0].getSourceFile().fileName;
75
+ const symbolName = symbol.getName();
76
+
77
+ // Track sub-symbol usage (Property Access)
78
+ if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
79
+ const parentType = this.checker.getTypeAtLocation(node.parent.expression);
80
+ const parentSymbol = parentType.getSymbol() || parentType.aliasSymbol;
81
+ if (parentSymbol) {
82
+ const parentDecl = parentSymbol.getDeclarations()?.[0];
83
+ if (parentDecl) {
84
+ const parentFile = parentDecl.getSourceFile().fileName;
85
+ fileNode.memberUsage = fileNode.memberUsage || new Set();
86
+ fileNode.memberUsage.add(`${parentFile}:${parentSymbol.getName()}.${symbolName}`);
87
+ }
88
+ }
79
89
  }
80
- }
81
- break;
82
- }
83
90
 
84
- // Handle Explicit Namespace Requirements: import config = require('./config');
85
- case ts.SyntaxKind.ImportEqualsDeclaration: {
86
- if (node.moduleReference && ts.isExternalModuleReference(node.moduleReference)) {
87
- if (node.moduleReference.expression && ts.isStringLiteral(node.moduleReference.expression)) {
88
- fileNode.explicitImports.add(node.moduleReference.expression.text);
91
+ if (declFile !== sourceFile.fileName) {
92
+ fileNode.resolvedReferences = fileNode.resolvedReferences || new Set();
93
+ fileNode.resolvedReferences.add(`${declFile}:${symbolName}`);
89
94
  }
90
95
  }
91
- break;
92
- }
93
-
94
- // Challenge #1: Tracking dynamic expressions e.g., import('./chunks/' + variant)
95
- case ts.SyntaxKind.CallExpression: {
96
- if (node.expression && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
97
- const firstArgument = node.arguments[0];
98
- if (firstArgument) {
99
- if (ts.isStringLiteral(firstArgument)) {
100
- fileNode.explicitImports.add(firstArgument.text);
101
- fileNode.dynamicImports.add(firstArgument.text);
102
- } else {
103
- // Deeply trace runtime calculated variables within the import parameters call
104
- const stringPatterns = [];
105
- this.traceStringExpressions(firstArgument, stringPatterns);
106
- fileNode.calculatedDynamicImports.push({
107
- rawText: firstArgument.getText(sourceFile),
108
- heuristics: stringPatterns,
109
- position: node.getStart(sourceFile)
110
- });
111
- }
112
- }
113
- }
114
- break;
115
96
  }
97
+ }
116
98
 
117
- // Handle Variable Declaration Assignments & Challenge #11 (AST Secret Scanning)
118
- case ts.SyntaxKind.VariableDeclaration: {
119
- if (node.name && ts.isIdentifier(node.name)) {
120
- this.auditAssignmentSafety(node.name.text, node.initializer, fileNode, sourceFile);
121
- } else if (node.name && (ts.isObjectBindingPattern(node.name) || ts.isArrayBindingPattern(node.name))) {
122
- // Flatten binding properties to map destructured usage accurately
123
- node.name.elements.forEach(element => {
124
- if (element.name && ts.isIdentifier(element.name)) {
125
- this.auditAssignmentSafety(element.name.text, null, fileNode, sourceFile);
126
- }
127
- });
128
- }
99
+ switch (node.kind) {
100
+ case ts.SyntaxKind.ImportDeclaration: {
101
+ this.handleImportDeclaration(node, fileNode);
129
102
  break;
130
103
  }
131
-
132
- // Handle Named Function Node Exports Matrix Configurations
133
- case ts.SyntaxKind.FunctionDeclaration: {
134
- if (node.name && ts.isIdentifier(node.name)) {
135
- const name = node.name.text;
136
- if (this.hasExportModifier(node)) {
137
- fileNode.internalExports.set(name, { type: 'function', start: node.getStart(sourceFile), end: node.getEnd() });
138
- }
139
- }
104
+ case ts.SyntaxKind.ExportDeclaration: {
105
+ this.handleExportDeclaration(node, fileNode, sourceFile);
140
106
  break;
141
107
  }
142
-
143
- // Handle Structural Class Definitions and Class Export Signatures
144
- case ts.SyntaxKind.ClassDeclaration: {
145
- if (node.name && ts.isIdentifier(node.name)) {
146
- const name = node.name.text;
147
- if (this.hasExportModifier(node)) {
148
- fileNode.internalExports.set(name, { type: 'class', start: node.getStart(sourceFile), end: node.getEnd() });
149
- }
150
- }
108
+ case ts.SyntaxKind.FunctionDeclaration:
109
+ case ts.SyntaxKind.ClassDeclaration:
110
+ case ts.SyntaxKind.InterfaceDeclaration:
111
+ case ts.SyntaxKind.TypeAliasDeclaration:
112
+ case ts.SyntaxKind.EnumDeclaration:
113
+ case ts.SyntaxKind.ModuleDeclaration: {
114
+ this.handleNamedDeclaration(node, fileNode, sourceFile);
151
115
  break;
152
116
  }
153
-
154
- // Handle Interface Definitions (Crucial for Challenge #10: Type Integrity Mapping)
155
- case ts.SyntaxKind.InterfaceDeclaration: {
156
- if (node.name && ts.isIdentifier(node.name)) {
157
- const name = node.name.text;
158
- if (this.hasExportModifier(node)) {
159
- fileNode.internalExports.set(name, { type: 'interface', start: node.getStart(sourceFile), end: node.getEnd() });
160
- }
161
- }
117
+ case ts.SyntaxKind.VariableStatement: {
118
+ this.handleVariableStatement(node, fileNode, sourceFile);
162
119
  break;
163
120
  }
164
-
165
- // Handle Type Invocations and Declarations Aliases
166
- case ts.SyntaxKind.TypeAliasDeclaration: {
167
- if (node.name && ts.isIdentifier(node.name)) {
168
- const name = node.name.text;
169
- if (this.hasExportModifier(node)) {
170
- fileNode.internalExports.set(name, { type: 'type-alias', start: node.getStart(sourceFile), end: node.getEnd() });
171
- }
172
- }
121
+ case ts.SyntaxKind.CallExpression: {
122
+ this.handleCallExpression(node, fileNode, sourceFile);
173
123
  break;
174
124
  }
125
+ }
175
126
 
176
- // Handle Explicit Export Assignments: export default baselineConfiguration;
177
- case ts.SyntaxKind.ExportAssignment: {
178
- const name = node.expression ? node.expression.getText(sourceFile) : 'default';
179
- fileNode.internalExports.set('default', {
180
- type: 'default-assignment',
181
- referencedSymbol: name,
182
- start: node.getStart(sourceFile),
183
- end: node.getEnd()
184
- });
185
- break;
186
- }
127
+ ts.forEachChild(node, child => this.walkNode(sourceFile, child, fileNode));
128
+ }
187
129
 
188
- // Handle Arbitrary String References to catch deep framework routing or dynamic keys
189
- case ts.SyntaxKind.StringLiteral: {
190
- const text = node.text;
191
- if (text.length > 2 && text.length < 120) {
192
- fileNode.rawStringReferences.add(text);
130
+ handleImportDeclaration(node, fileNode) {
131
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
132
+ const specifier = node.moduleSpecifier.text;
133
+ fileNode.explicitImports.add(specifier);
134
+
135
+ if (node.importClause) {
136
+ if (node.importClause.namedBindings) {
137
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
138
+ node.importClause.namedBindings.elements.forEach(element => {
139
+ const importedName = element.name.text;
140
+ const propertyName = element.propertyName ? element.propertyName.text : importedName;
141
+ fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
142
+ });
143
+ } else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
144
+ fileNode.importedSymbols.add(`${specifier}:*`);
145
+ }
193
146
  }
194
- break;
195
- }
196
-
197
- // Track general identifiers to register references to mapped import keys
198
- case ts.SyntaxKind.Identifier: {
199
- const idText = node.text;
200
- // Avoid adding declarations to usage logs to keep verification accurate
201
- if (!this.isNodeDeclarationName(node)) {
202
- fileNode.instantiatedIdentifiers.add(idText);
147
+ if (node.importClause.name) {
148
+ fileNode.importedSymbols.add(`${specifier}:default`);
203
149
  }
204
- break;
205
150
  }
206
151
  }
207
-
208
- // Traverse recursively down the Node structural tree
209
- ts.forEachChild(node, child => this.walkNode(sourceFile, child, fileNode));
210
152
  }
211
153
 
212
- /**
213
- * Challenge #1: Evaluates math operations and template configurations inside dynamic imports.
214
- */
215
- traceStringExpressions(node, collector) {
216
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
217
- this.traceStringExpressions(node.left, collector);
218
- this.traceStringExpressions(node.right, collector);
219
- } else if (ts.isStringLiteral(node)) {
220
- collector.push({ type: 'literal', val: node.text });
221
- } else if (ts.isTemplateExpression(node)) {
222
- if (node.head) collector.push({ type: 'template-slice', val: node.head.text });
223
- node.templateSpans.forEach(span => {
224
- collector.push({ type: 'dynamic-var', val: span.expression.getText() });
225
- if (span.literal) collector.push({ type: 'template-slice', val: span.literal.text });
226
- });
227
- } else {
228
- collector.push({ type: 'computed-variable', val: node.getText() });
154
+ handleExportDeclaration(node, fileNode, sourceFile) {
155
+ // Handle re-exports: export { x } from './y'
156
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
157
+ const specifier = node.moduleSpecifier.text;
158
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
159
+ node.exportClause.elements.forEach(element => {
160
+ const name = element.name.text;
161
+ fileNode.internalExports.set(name, {
162
+ type: 're-export',
163
+ source: specifier,
164
+ start: node.getStart(sourceFile),
165
+ end: node.getEnd()
166
+ });
167
+ });
168
+ } else {
169
+ // export * from './y'
170
+ fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
171
+ }
229
172
  }
230
173
  }
231
174
 
232
- /**
233
- * Challenge #11: AST Secret Scanning. Evaluates entropy and patterns directly via assignments.
234
- */
235
- auditAssignmentSafety(variableName, initializer, fileNode, sourceFile) {
236
- // Process variable mapping target indicators first
237
- if (this.hasExportModifier(variableName?.parent)) {
238
- fileNode.internalExports.set(variableName, { type: 'variable', start: variableName.parent.getStart(sourceFile), end: variableName.parent.getEnd() });
239
- }
240
-
241
- if (!initializer || !ts.isStringLiteral(initializer)) return;
242
- const value = initializer.text;
175
+ handleNamedDeclaration(node, fileNode, sourceFile) {
176
+ if (this.hasExportModifier(node)) {
177
+ const isDefault = node.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
178
+ const name = isDefault ? 'default' : (node.name?.text || 'anonymous');
179
+
180
+ const exportInfo = {
181
+ type: ts.SyntaxKind[node.kind].toLowerCase().replace('declaration', ''),
182
+ start: node.getStart(sourceFile),
183
+ end: node.getEnd()
184
+ };
243
185
 
244
- // Challenge #11 Heuristic validation parameters matching variable patterns or contents values
245
- const isSuspiciousKeyName = /api_?key|secret|token|password|auth_?token|private_?key/i.test(variableName);
246
- const entropy = this.calculateShannonEntropy(value);
186
+ fileNode.internalExports.set(name, exportInfo);
187
+
188
+ // Phase 4: Drill down into members
189
+ if (ts.isEnumDeclaration(node)) {
190
+ exportInfo.members = node.members.map(m => m.name.getText(sourceFile));
191
+ } else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
192
+ exportInfo.members = node.members
193
+ .filter(m => m.name)
194
+ .map(m => m.name.getText(sourceFile));
195
+ } else if (ts.isModuleDeclaration(node)) {
196
+ // Handle Namespaces
197
+ const members = [];
198
+ if (node.body && ts.isModuleBlock(node.body)) {
199
+ node.body.statements.forEach(stmt => {
200
+ if (this.hasExportModifier(stmt) && (ts.isVariableStatement(stmt) || ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt))) {
201
+ if (ts.isVariableStatement(stmt)) {
202
+ stmt.declarationList.declarations.forEach(d => members.push(d.name.getText(sourceFile)));
203
+ } else if (stmt.name) {
204
+ members.push(stmt.name.getText(sourceFile));
205
+ }
206
+ }
207
+ });
208
+ }
209
+ exportInfo.members = members;
210
+ }
247
211
 
248
- if ((isSuspiciousKeyName && value.length > 8) || (entropy > this.entropyThreshold && value.length > 16)) {
249
- fileNode.securityThreats.push({
250
- identifier: variableName,
251
- entropy: parseFloat(entropy.toFixed(2)),
252
- position: initializer.getStart(sourceFile),
253
- riskCode: 'HIGH_RISK_SECRET_LEAK'
254
- });
212
+ const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
213
+ fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
255
214
  }
256
215
  }
257
216
 
258
- calculateShannonEntropy(str) {
259
- const map = {};
260
- for (let i = 0; i < str.length; i++) {
261
- const char = str[i];
262
- map[char] = (map[char] || 0) + 1;
263
- }
264
- let entropy = 0;
265
- for (const char in map) {
266
- const p = map[char] / str.length;
267
- entropy -= p * Math.log2(p);
217
+ handleVariableStatement(node, fileNode, sourceFile) {
218
+ if (this.hasExportModifier(node)) {
219
+ node.declarationList.declarations.forEach(decl => {
220
+ if (decl.name && ts.isIdentifier(decl.name)) {
221
+ const name = decl.name.text;
222
+ fileNode.internalExports.set(name, {
223
+ type: 'variable',
224
+ start: decl.getStart(sourceFile),
225
+ end: decl.getEnd()
226
+ });
227
+ }
228
+ });
268
229
  }
269
- return entropy;
270
230
  }
271
231
 
272
- /**
273
- * Challenge #18 & #8: Parse JSDoc suppression blocks right out of code statements.
274
- */
275
- extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
276
- const fullText = sourceFile.text;
277
- const commentRanges = ts.getLeadingCommentRanges(fullText, 0) || [];
278
-
279
- for (const range of commentRanges) {
280
- const comment = fullText.slice(range.pos, range.end);
281
- const matches = comment.match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
282
- if (matches) {
283
- matches.forEach(m => {
284
- const directive = m.replace('@scaffold-suppress', '').trim();
285
- fileNode.localSuppressedRules.add(directive);
286
- });
232
+ handleCallExpression(node, fileNode, sourceFile) {
233
+ // Trace dynamic imports
234
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
235
+ const arg = node.arguments[0];
236
+ if (arg && ts.isStringLiteral(arg)) {
237
+ fileNode.explicitImports.add(arg.text);
238
+ fileNode.dynamicImports.add(arg.text);
287
239
  }
288
240
  }
289
241
  }
290
242
 
291
243
  hasExportModifier(node) {
292
- if (!node || !node.modifiers) return false;
293
- return node.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
244
+ return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
294
245
  }
295
246
 
296
247
  isNodeDeclarationName(node) {
297
248
  const parent = node.parent;
298
249
  if (!parent) return false;
299
- if (ts.isVariableDeclaration(parent) && parent.name === node) return true;
300
- if (ts.isFunctionDeclaration(parent) && parent.name === node) return true;
301
- if (ts.isClassDeclaration(parent) && parent.name === node) return true;
302
- if (ts.isInterfaceDeclaration(parent) && parent.name === node) return true;
303
- if (ts.isImportSpecifier(parent) && parent.name === node) return true;
304
- return false;
250
+ return (ts.isVariableDeclaration(parent) || ts.isFunctionDeclaration(parent) ||
251
+ ts.isClassDeclaration(parent) || ts.isInterfaceDeclaration(parent) ||
252
+ ts.isEnumDeclaration(parent) || ts.isModuleDeclaration(parent)) && parent.name === node;
305
253
  }
306
254
 
307
- getScriptKind(filePath) {
308
- if (filePath.endsWith('.ts')) return ts.ScriptKind.TS;
309
- if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX;
310
- if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX;
311
- return ts.ScriptKind.JS;
255
+ extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
256
+ const fullText = sourceFile.text;
257
+ const commentRegex = /\/\*\*?[\s\S]*?\*\/|\/\/.*/g;
258
+ let match;
259
+ while ((match = commentRegex.exec(fullText)) !== null) {
260
+ const suppressMatches = match[0].match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
261
+ if (suppressMatches) {
262
+ suppressMatches.forEach(m => fileNode.localSuppressedRules.add(m.replace('@scaffold-suppress', '').trim()));
263
+ }
264
+ }
312
265
  }
313
266
  }
@@ -104,6 +104,18 @@ export class BarrelParser {
104
104
  }
105
105
  break;
106
106
  }
107
+
108
+ // Track default exports declared directly within the file boundary:
109
+ // Handles both `export default function foo()` and `export default expression`.
110
+ // The canonical symbol name stored is 'default' so that forwardedNamedExports
111
+ // entries whose sourceSymbol is 'default' can resolve correctly via Rule A.
112
+ case ts.SyntaxKind.ExportAssignment: {
113
+ // ExportAssignment covers `export default <expr>` (isExportEquals === false)
114
+ // as well as `export = <expr>` (isExportEquals === true, CommonJS style).
115
+ // We register 'default' for both to ensure the barrel tracer can settle here.
116
+ spec.declaredLocalExports.add('default');
117
+ break;
118
+ }
107
119
  }
108
120
 
109
121
  ts.forEachChild(node, child => this.harvestExportSignatures(child, spec));