@@ -23,9 +23,19 @@ interface TsDiagnostic {
2323 code : number ;
2424 messageText : string | TsDiagnosticMessageChain ;
2525}
26+ interface TsNode {
27+ kind : number ;
28+ pos : number ;
29+ end : number ;
30+ }
31+ interface TsSourceFile extends TsNode {
32+ fileName : string ;
33+ getLineAndCharacterOfPosition ( pos : number ) : { line : number ; character : number } ;
34+ }
2635interface TsProgram {
2736 getSyntacticDiagnostics ( ) : readonly TsDiagnostic [ ] ;
2837 getSemanticDiagnostics ( ) : readonly TsDiagnostic [ ] ;
38+ getSourceFiles ( ) : readonly TsSourceFile [ ] ;
2939}
3040interface TsApi {
3141 sys : TsSystem ;
@@ -48,6 +58,16 @@ interface TsApi {
4858 messageText : string | TsDiagnosticMessageChain ,
4959 newLine : string ,
5060 ) : string ;
61+ SyntaxKind : {
62+ readonly NewExpression : number ;
63+ readonly CallExpression : number ;
64+ readonly FunctionDeclaration : number ;
65+ readonly FunctionExpression : number ;
66+ readonly ArrowFunction : number ;
67+ readonly MethodDeclaration : number ;
68+ readonly Constructor : number ;
69+ } ;
70+ forEachChild < T > ( node : TsNode , cbNode : ( node : TsNode ) => T | undefined ) : T | undefined ;
5171}
5272
5373/**
@@ -85,7 +105,7 @@ export class StaticScanner extends EventEmitter {
85105 }
86106
87107 /**
88- * Run a full scan (TypeScript + ESLint if available).
108+ * Run a full scan (TypeScript + ESLint + connection-pool static rules if available).
89109 */
90110 public async scan ( ) : Promise < ScanResult [ ] > {
91111 const results : ScanResult [ ] = [ ] ;
@@ -98,6 +118,11 @@ export class StaticScanner extends EventEmitter {
98118 results . push ( eslintResult ) ;
99119 }
100120
121+ const poolResult = await this . runConnectionPoolScan ( ) ;
122+ if ( poolResult && poolResult . totalIssues > 0 ) {
123+ results . push ( poolResult ) ;
124+ }
125+
101126 this . emit ( "scan" , results ) ;
102127 return results ;
103128 }
@@ -250,6 +275,113 @@ export class StaticScanner extends EventEmitter {
250275 } ) ;
251276 }
252277
278+ /**
279+ * R.2 — Scan TypeScript source files for connection constructors called inside
280+ * function bodies (missing-connection-pool). Uses the TypeScript Compiler API
281+ * to walk the AST; returns null if TypeScript is not available.
282+ */
283+ public async runConnectionPoolScan ( ) : Promise < ScanResult | null > {
284+ const start = performance . now ( ) ;
285+
286+ try {
287+ const tsId = "typescript" ;
288+ const tsModule : unknown = await import ( tsId ) ;
289+ const ts = ( ( tsModule as { default ?: TsApi } ) . default ?? tsModule ) as TsApi ;
290+
291+ const configPath = ts . findConfigFile (
292+ this . targetDir ,
293+ ( p : string ) => ts . sys . fileExists ( p ) ,
294+ "tsconfig.json" ,
295+ ) ;
296+ if ( ! configPath ) return null ;
297+
298+ const { config, error : readError } = ts . readConfigFile (
299+ configPath ,
300+ ( p : string , enc ?: string ) => ts . sys . readFile ( p , enc ) ,
301+ ) ;
302+ if ( readError ) return null ;
303+
304+ const parsed = ts . parseJsonConfigFileContent ( config as object , ts . sys , dirname ( configPath ) ) ;
305+ const program = ts . createProgram ( parsed . fileNames , { ...parsed . options , noEmit : true } ) ;
306+
307+ const suggestions = this . detectConnectionInFunction ( ts , program ) ;
308+
309+ return {
310+ tool : "argus-static" ,
311+ totalIssues : suggestions . length ,
312+ suggestions,
313+ durationMs : performance . now ( ) - start ,
314+ } ;
315+ } catch {
316+ return null ;
317+ }
318+ }
319+
320+ private detectConnectionInFunction ( ts : TsApi , program : TsProgram ) : FixSuggestion [ ] {
321+ const suggestions : FixSuggestion [ ] = [ ] ;
322+
323+ const CONNECTION_CTORS = new Set ( [
324+ "Client" ,
325+ "Connection" ,
326+ "Sequelize" ,
327+ "MongoClient" ,
328+ "createConnection" ,
329+ "createPool" ,
330+ ] ) ;
331+
332+ const FUNCTION_KINDS = new Set ( [
333+ ts . SyntaxKind . FunctionDeclaration ,
334+ ts . SyntaxKind . FunctionExpression ,
335+ ts . SyntaxKind . ArrowFunction ,
336+ ts . SyntaxKind . MethodDeclaration ,
337+ ts . SyntaxKind . Constructor ,
338+ ] ) ;
339+
340+ const walk = ( node : TsNode , sourceFile : TsSourceFile , insideFunction : boolean ) : void => {
341+ const enterFunction = FUNCTION_KINDS . has ( node . kind ) ;
342+ const nowInside = insideFunction || enterFunction ;
343+ const kindNew = ts . SyntaxKind . NewExpression ;
344+ const kindCall = ts . SyntaxKind . CallExpression ;
345+
346+ if ( insideFunction && ( node . kind === kindNew || node . kind === kindCall ) ) {
347+ const expr = ( node as { expression ?: { text ?: string } } ) . expression ;
348+ const name = expr ?. text ;
349+
350+ if ( name && CONNECTION_CTORS . has ( name ) ) {
351+ const { line, character } = sourceFile . getLineAndCharacterOfPosition ( node . pos ) ;
352+ const prefix = node . kind === kindNew ? `new ${ name } ()` : `${ name } ()` ;
353+ suggestions . push ( {
354+ severity : "warning" ,
355+ rule : "missing-connection-pool" ,
356+ message : `${ prefix } called inside a function body — creates a new connection per call instead of reusing a pool.` ,
357+ suggestedFix :
358+ "Move the client/pool instantiation to module scope and reuse it across requests." ,
359+ location : `${ sourceFile . fileName } :${ line + 1 } :${ character + 1 } ` ,
360+ } ) ;
361+ }
362+ }
363+
364+ ts . forEachChild ( node , ( child ) => {
365+ walk ( child , sourceFile , nowInside ) ;
366+ return undefined ;
367+ } ) ;
368+ } ;
369+
370+ for ( const sourceFile of program . getSourceFiles ( ) ) {
371+ if (
372+ sourceFile . fileName . includes ( "node_modules" ) ||
373+ sourceFile . fileName . endsWith ( ".d.ts" ) ||
374+ sourceFile . fileName . endsWith ( ".test.ts" ) ||
375+ sourceFile . fileName . endsWith ( ".spec.ts" )
376+ )
377+ continue ;
378+
379+ walk ( sourceFile as TsNode , sourceFile , false ) ;
380+ }
381+
382+ return suggestions ;
383+ }
384+
253385 /**
254386 * Parse TypeScript compiler output into FixSuggestions.
255387 * Format: `filepath(line,col): error TSxxxx: message`
0 commit comments