|
7 | 7 |
|
8 | 8 | import * as fs from 'node:fs/promises'; |
9 | 9 | import * as path from 'node:path'; |
| 10 | +import { |
| 11 | + ERROR_HANDLING_QUERIES, |
| 12 | + IMPORT_STYLE_QUERIES, |
| 13 | + TYPE_COVERAGE_QUERIES, |
| 14 | +} from '../pattern-matcher/rules'; |
| 15 | +import type { PatternMatcher, PatternMatchRule } from '../pattern-matcher/wasm-matcher'; |
| 16 | +import { resolveLanguage } from '../pattern-matcher/wasm-matcher'; |
10 | 17 | import { scanRepository } from '../scanner'; |
11 | 18 | import type { Document } from '../scanner/types'; |
12 | 19 | import { findTestFile, isTestFile } from '../utils/test-utils'; |
@@ -102,6 +109,121 @@ export function extractTypeCoverageFromSignatures(signatures: string[]): TypeAnn |
102 | 109 | return { coverage, annotatedCount: annotated.length, totalCount: signatures.length }; |
103 | 110 | } |
104 | 111 |
|
| 112 | +// ======================================================================== |
| 113 | +// AST-Enhanced Extractors — use PatternMatcher when available, regex fallback |
| 114 | +// ======================================================================== |
| 115 | + |
| 116 | +/** |
| 117 | + * Run AST queries if matcher and language are available, else return empty map. |
| 118 | + */ |
| 119 | +async function runAstQueries( |
| 120 | + content: string, |
| 121 | + filePath: string | undefined, |
| 122 | + matcher: PatternMatcher | undefined, |
| 123 | + queries: PatternMatchRule[] |
| 124 | +): Promise<Map<string, number>> { |
| 125 | + if (!matcher || !filePath) return new Map(); |
| 126 | + const language = resolveLanguage(filePath); |
| 127 | + if (!language) return new Map(); |
| 128 | + return matcher.match(content, language, queries); |
| 129 | +} |
| 130 | + |
| 131 | +/** |
| 132 | + * Extract error handling using AST (preferred) + regex (fallback/supplement). |
| 133 | + */ |
| 134 | +export async function extractErrorHandlingWithAst( |
| 135 | + content: string, |
| 136 | + filePath?: string, |
| 137 | + matcher?: PatternMatcher |
| 138 | +): Promise<ErrorHandlingPattern> { |
| 139 | + const regex = extractErrorHandlingFromContent(content); |
| 140 | + const ast = await runAstQueries(content, filePath, matcher, ERROR_HANDLING_QUERIES); |
| 141 | + |
| 142 | + if (ast.size === 0) return regex; |
| 143 | + |
| 144 | + const hasThrow = (ast.get('throw') ?? 0) > 0; |
| 145 | + const hasTryCatch = (ast.get('try-catch') ?? 0) > 0; |
| 146 | + const hasPromiseCatch = (ast.get('promise-catch') ?? 0) > 0; |
| 147 | + const hasResultRegex = regex.style === 'result'; |
| 148 | + |
| 149 | + // Classification: throw is the style, try-catch is the mechanism |
| 150 | + if (hasThrow && (hasTryCatch || hasPromiseCatch || hasResultRegex)) { |
| 151 | + return { style: 'mixed', examples: [] }; |
| 152 | + } |
| 153 | + if (hasThrow) return { style: 'throw', examples: [] }; |
| 154 | + if (hasResultRegex) return regex; // AST can't detect Result<T>, keep regex |
| 155 | + if (hasTryCatch || hasPromiseCatch) return { style: 'throw', examples: [] }; |
| 156 | + |
| 157 | + return regex; |
| 158 | +} |
| 159 | + |
| 160 | +/** |
| 161 | + * Extract import style using AST (preferred) + regex (fallback/supplement). |
| 162 | + */ |
| 163 | +export async function extractImportStyleWithAst( |
| 164 | + content: string, |
| 165 | + filePath?: string, |
| 166 | + matcher?: PatternMatcher |
| 167 | +): Promise<ImportStylePattern> { |
| 168 | + const regex = extractImportStyleFromContent(content); |
| 169 | + const ast = await runAstQueries(content, filePath, matcher, IMPORT_STYLE_QUERIES); |
| 170 | + |
| 171 | + if (ast.size === 0) return regex; |
| 172 | + |
| 173 | + const dynamicImports = ast.get('dynamic-import') ?? 0; |
| 174 | + const reExports = ast.get('re-export') ?? 0; |
| 175 | + const requires = ast.get('require') ?? 0; |
| 176 | + |
| 177 | + // Dynamic imports count as ESM |
| 178 | + const esmCount = |
| 179 | + (regex.style === 'esm' || regex.style === 'mixed' ? regex.importCount : 0) + |
| 180 | + dynamicImports + |
| 181 | + reExports; |
| 182 | + const cjsCount = requires; |
| 183 | + |
| 184 | + if (esmCount === 0 && cjsCount === 0) return regex; |
| 185 | + |
| 186 | + const hasESM = esmCount > 0 || regex.style === 'esm' || regex.style === 'mixed'; |
| 187 | + const hasCJS = cjsCount > 0 || regex.style === 'cjs' || regex.style === 'mixed'; |
| 188 | + const style: ImportStylePattern['style'] = hasESM && hasCJS ? 'mixed' : hasESM ? 'esm' : 'cjs'; |
| 189 | + return { style, importCount: regex.importCount + dynamicImports }; |
| 190 | +} |
| 191 | + |
| 192 | +/** |
| 193 | + * Extract type coverage using AST (preferred) + regex signatures (supplement). |
| 194 | + */ |
| 195 | +export async function extractTypeCoverageWithAst( |
| 196 | + content: string, |
| 197 | + filePath?: string, |
| 198 | + matcher?: PatternMatcher, |
| 199 | + signatures?: string[] |
| 200 | +): Promise<TypeAnnotationPattern> { |
| 201 | + // Start with signature-based detection (from index or ts-morph) |
| 202 | + const regex = extractTypeCoverageFromSignatures(signatures ?? []); |
| 203 | + const ast = await runAstQueries(content, filePath, matcher, TYPE_COVERAGE_QUERIES); |
| 204 | + |
| 205 | + if (ast.size === 0) return regex; |
| 206 | + |
| 207 | + const arrowTyped = ast.get('arrow-return-type') ?? 0; |
| 208 | + const functionTyped = ast.get('function-return-type') ?? 0; |
| 209 | + const astAnnotated = arrowTyped + functionTyped; |
| 210 | + |
| 211 | + // Merge: use the higher count (AST catches arrows that signatures miss) |
| 212 | + const annotatedCount = Math.max(regex.annotatedCount, astAnnotated); |
| 213 | + const totalCount = Math.max(regex.totalCount, astAnnotated); // at least as many as annotated |
| 214 | + |
| 215 | + if (totalCount === 0) return regex; |
| 216 | + |
| 217 | + const ratio = annotatedCount / totalCount; |
| 218 | + let coverage: TypeAnnotationPattern['coverage']; |
| 219 | + if (ratio >= 0.9) coverage = 'full'; |
| 220 | + else if (ratio >= 0.5) coverage = 'partial'; |
| 221 | + else if (ratio > 0) coverage = 'minimal'; |
| 222 | + else coverage = 'none'; |
| 223 | + |
| 224 | + return { coverage, annotatedCount, totalCount }; |
| 225 | +} |
| 226 | + |
105 | 227 | /** |
106 | 228 | * Pattern Analysis Service |
107 | 229 | * |
@@ -160,12 +282,18 @@ export class PatternAnalysisService { |
160 | 282 | .map((d) => (d.metadata.signature as string) || '') |
161 | 283 | .filter(Boolean); |
162 | 284 |
|
| 285 | + const [importStyle, errorHandling, typeAnnotations] = await Promise.all([ |
| 286 | + extractImportStyleWithAst(content, filePath, this.config.patternMatcher), |
| 287 | + extractErrorHandlingWithAst(content, filePath, this.config.patternMatcher), |
| 288 | + extractTypeCoverageWithAst(content, filePath, this.config.patternMatcher, signatures), |
| 289 | + ]); |
| 290 | + |
163 | 291 | return { |
164 | 292 | fileSize: { lines, bytes }, |
165 | 293 | testing, |
166 | | - importStyle: extractImportStyleFromContent(content), |
167 | | - errorHandling: extractErrorHandlingFromContent(content), |
168 | | - typeAnnotations: extractTypeCoverageFromSignatures(signatures), |
| 294 | + importStyle, |
| 295 | + errorHandling, |
| 296 | + typeAnnotations, |
169 | 297 | }; |
170 | 298 | } |
171 | 299 |
|
@@ -256,12 +384,18 @@ export class PatternAnalysisService { |
256 | 384 | .map((d) => d.metadata.signature || '') |
257 | 385 | .filter(Boolean); |
258 | 386 |
|
| 387 | + const [importStyle, errorHandling, typeAnnotations] = await Promise.all([ |
| 388 | + extractImportStyleWithAst(content, filePath, this.config.patternMatcher), |
| 389 | + extractErrorHandlingWithAst(content, filePath, this.config.patternMatcher), |
| 390 | + extractTypeCoverageWithAst(content, filePath, this.config.patternMatcher, signatures), |
| 391 | + ]); |
| 392 | + |
259 | 393 | return { |
260 | 394 | fileSize: { lines: content.split('\n').length, bytes: stat.size }, |
261 | 395 | testing, |
262 | | - importStyle: extractImportStyleFromContent(content), |
263 | | - errorHandling: extractErrorHandlingFromContent(content), |
264 | | - typeAnnotations: extractTypeCoverageFromSignatures(signatures), |
| 396 | + importStyle, |
| 397 | + errorHandling, |
| 398 | + typeAnnotations, |
265 | 399 | }; |
266 | 400 | } |
267 | 401 |
|
|
0 commit comments