1010import { readdirSync , readFileSync } from 'node:fs'
1111import { dirname , extname , join , relative , resolve } from 'node:path'
1212import { fileURLToPath } from 'node:url'
13- import * as ts from 'typescript '
13+ import { parse } from '@babel/parser '
1414
1515const SCRIPT_DIR = dirname ( fileURLToPath ( import . meta. url ) )
1616const ROOT = resolve ( SCRIPT_DIR , '..' )
@@ -25,6 +25,13 @@ interface Violation {
2525 expression : string
2626}
2727
28+ interface SyntaxNode extends Record < string , unknown > {
29+ type : string
30+ start ?: number | null
31+ end ?: number | null
32+ loc ?: { start : { line : number } } | null
33+ }
34+
2835function isProductionSource ( path : string ) : boolean {
2936 const normalized = path . replaceAll ( '\\' , '/' )
3037 return (
@@ -47,113 +54,197 @@ function collectProductionSources(dir: string, found: string[] = []): string[] {
4754 return found
4855}
4956
50- function unwrapExpression ( expression : ts . Expression ) : ts . Expression {
57+ function isSyntaxNode ( value : unknown ) : value is SyntaxNode {
58+ return (
59+ typeof value === 'object' && value !== null && 'type' in value && typeof value . type === 'string'
60+ )
61+ }
62+
63+ function getChildNodes ( node : SyntaxNode ) : SyntaxNode [ ] {
64+ const children : SyntaxNode [ ] = [ ]
65+ for ( const value of Object . values ( node ) ) {
66+ if ( Array . isArray ( value ) ) {
67+ for ( const item of value ) {
68+ if ( isSyntaxNode ( item ) ) children . push ( item )
69+ }
70+ } else if ( isSyntaxNode ( value ) ) {
71+ children . push ( value )
72+ }
73+ }
74+ return children
75+ }
76+
77+ function unwrapExpression ( expression : SyntaxNode ) : SyntaxNode {
5178 let current = expression
5279 while (
53- ts . isParenthesizedExpression ( current ) ||
54- ts . isAsExpression ( current ) ||
55- ts . isTypeAssertionExpression ( current ) ||
56- ts . isNonNullExpression ( current )
80+ [
81+ 'ParenthesizedExpression' ,
82+ 'TSAsExpression' ,
83+ 'TSTypeAssertion' ,
84+ 'TSNonNullExpression' ,
85+ 'TSSatisfiesExpression' ,
86+ 'TypeCastExpression' ,
87+ ] . includes ( current . type ) &&
88+ isSyntaxNode ( current . expression )
5789 ) {
5890 current = current . expression
5991 }
6092 return current
6193}
6294
6395function getStaticMemberAccess (
64- expression : ts . Expression
65- ) : { target : ts . Expression ; member : string } | undefined {
96+ expression : SyntaxNode
97+ ) : { target : SyntaxNode ; member : string } | undefined {
6698 const current = unwrapExpression ( expression )
67- if ( ts . isPropertyAccessExpression ( current ) ) {
68- return { target : current . expression , member : current . name . text }
69- }
7099 if (
71- ts . isElementAccessExpression ( current ) &&
72- current . argumentExpression &&
73- ( ts . isStringLiteral ( current . argumentExpression ) ||
74- ts . isNoSubstitutionTemplateLiteral ( current . argumentExpression ) )
100+ ( current . type === 'MemberExpression' || current . type === 'OptionalMemberExpression' ) &&
101+ isSyntaxNode ( current . object ) &&
102+ isSyntaxNode ( current . property )
75103 ) {
76- return { target : current . expression , member : current . argumentExpression . text }
104+ const property = current . property
105+ if (
106+ current . computed === false &&
107+ property . type === 'Identifier' &&
108+ typeof property . name === 'string'
109+ ) {
110+ return { target : current . object , member : property . name }
111+ }
112+ if (
113+ current . computed === true &&
114+ property . type === 'StringLiteral' &&
115+ typeof property . value === 'string'
116+ ) {
117+ return { target : current . object , member : property . value }
118+ }
119+ if (
120+ current . computed === true &&
121+ property . type === 'TemplateLiteral' &&
122+ Array . isArray ( property . expressions ) &&
123+ property . expressions . length === 0 &&
124+ Array . isArray ( property . quasis ) &&
125+ property . quasis . length === 1 &&
126+ isSyntaxNode ( property . quasis [ 0 ] )
127+ ) {
128+ const value = property . quasis [ 0 ] . value
129+ if (
130+ typeof value === 'object' &&
131+ value !== null &&
132+ 'cooked' in value &&
133+ typeof value . cooked === 'string'
134+ ) {
135+ return { target : current . object , member : value . cooked }
136+ }
137+ }
77138 }
78139 return undefined
79140}
80141
81- function isLikelyToolIdentifier ( expression : ts . Expression ) : boolean {
142+ function isLikelyToolIdentifier ( expression : SyntaxNode ) : boolean {
82143 const current = unwrapExpression ( expression )
83- return ts . isIdentifier ( current ) && ( current . text === 'tool' || current . text . endsWith ( 'Tool' ) )
144+ return (
145+ current . type === 'Identifier' &&
146+ typeof current . name === 'string' &&
147+ ( current . name === 'tool' || current . name . endsWith ( 'Tool' ) )
148+ )
84149}
85150
86151export function findToolRequestBoundaryViolations ( source : string , file = 'source.ts' ) : Violation [ ] {
87152 const extension = extname ( file )
88- const scriptKind =
89- extension === '.tsx'
90- ? ts . ScriptKind . TSX
91- : extension === '.jsx'
92- ? ts . ScriptKind . JSX
93- : [ '.js' , '.mjs' , '.cjs' ] . includes ( extension )
94- ? ts . ScriptKind . JS
95- : ts . ScriptKind . TS
96- const sourceFile = ts . createSourceFile ( file , source , ts . ScriptTarget . Latest , true , scriptKind )
153+ const syntaxTree = parse ( source , {
154+ sourceFilename : file ,
155+ sourceType : 'unambiguous' ,
156+ errorRecovery : true ,
157+ plugins : [
158+ ... ( extension === '.jsx' || extension === '.tsx' ? ( [ 'jsx' ] as const ) : [ ] ) ,
159+ ... ( ! [ '.js' , '.jsx' , '.mjs' , '.cjs' ] . includes ( extension ) ? ( [ 'typescript' ] as const ) : [ ] ) ,
160+ ] ,
161+ } )
97162 const requestAliases = new Set < string > ( )
98163 const violations : Violation [ ] = [ ]
99164 const seen = new Set < number > ( )
100165
101- const report = ( node : ts . Node ) => {
102- if ( seen . has ( node . getStart ( sourceFile ) ) ) return
103- seen . add ( node . getStart ( sourceFile ) )
104- const { line } = sourceFile . getLineAndCharacterOfPosition ( node . getStart ( sourceFile ) )
166+ const report = ( node : SyntaxNode ) => {
167+ if ( typeof node . start !== 'number' || typeof node . end !== 'number' || ! node . loc ) return
168+ if ( seen . has ( node . start ) ) return
169+ seen . add ( node . start )
105170 violations . push ( {
106171 file,
107- line : line + 1 ,
108- expression : node . getText ( sourceFile ) ,
172+ line : node . loc . start . line ,
173+ expression : source . slice ( node . start , node . end ) ,
109174 } )
110175 }
111176
112- const collectAliases = ( node : ts . Node ) => {
113- if ( ts . isVariableDeclaration ( node ) && ts . isIdentifier ( node . name ) && node . initializer ) {
114- const access = getStaticMemberAccess ( node . initializer )
177+ const collectAliases = ( node : SyntaxNode ) => {
178+ if (
179+ node . type === 'VariableDeclarator' &&
180+ isSyntaxNode ( node . id ) &&
181+ node . id . type === 'Identifier' &&
182+ typeof node . id . name === 'string' &&
183+ isSyntaxNode ( node . init )
184+ ) {
185+ const access = getStaticMemberAccess ( node . init )
115186 if ( access ?. member === 'request' && isLikelyToolIdentifier ( access . target ) ) {
116- requestAliases . add ( node . name . text )
187+ requestAliases . add ( node . id . name )
117188 }
118189 }
119- ts . forEachChild ( node , collectAliases )
190+ for ( const child of getChildNodes ( node ) ) collectAliases ( child )
120191 }
121- collectAliases ( sourceFile )
192+ collectAliases ( syntaxTree . program )
122193
123- const visit = ( node : ts . Node ) => {
194+ const visit = ( node : SyntaxNode ) => {
124195 if (
125- ts . isVariableDeclaration ( node ) &&
126- ts . isObjectBindingPattern ( node . name ) &&
127- node . initializer
196+ node . type === 'VariableDeclarator' &&
197+ isSyntaxNode ( node . id ) &&
198+ node . id . type === 'ObjectPattern' &&
199+ isSyntaxNode ( node . init )
128200 ) {
129- const sourceAccess = getStaticMemberAccess ( node . initializer )
201+ const sourceAccess = getStaticMemberAccess ( node . init )
130202 const sourceIsToolRequest =
131203 sourceAccess ?. member === 'request' && isLikelyToolIdentifier ( sourceAccess . target )
132- const source = unwrapExpression ( node . initializer )
133- const sourceIsToolRequestAlias = ts . isIdentifier ( source ) && requestAliases . has ( source . text )
204+ const initializer = unwrapExpression ( node . init )
205+ const sourceIsToolRequestAlias =
206+ initializer . type === 'Identifier' &&
207+ typeof initializer . name === 'string' &&
208+ requestAliases . has ( initializer . name )
134209 if ( sourceIsToolRequest || sourceIsToolRequestAlias ) {
135- for ( const element of node . name . elements ) {
136- const property = element . propertyName ?? element . name
137- if ( ts . isIdentifier ( property ) && REQUEST_MEMBERS . has ( property . text ) ) report ( element )
210+ const properties = Array . isArray ( node . id . properties ) ? node . id . properties : [ ]
211+ for ( const property of properties ) {
212+ if (
213+ ! isSyntaxNode ( property ) ||
214+ property . type !== 'ObjectProperty' ||
215+ ! isSyntaxNode ( property . key )
216+ ) {
217+ continue
218+ }
219+ const key = property . key
220+ const member =
221+ key . type === 'Identifier' && typeof key . name === 'string'
222+ ? key . name
223+ : key . type === 'StringLiteral' && typeof key . value === 'string'
224+ ? key . value
225+ : undefined
226+ if ( member && REQUEST_MEMBERS . has ( member ) ) report ( property )
138227 }
139228 }
140229 }
141- if ( ts . isPropertyAccessExpression ( node ) || ts . isElementAccessExpression ( node ) ) {
230+ if ( node . type === 'MemberExpression' || node . type === 'OptionalMemberExpression' ) {
142231 const access = getStaticMemberAccess ( node )
143232 if ( access && REQUEST_MEMBERS . has ( access . member ) ) {
144233 const target = unwrapExpression ( access . target )
145234 const targetAccess = getStaticMemberAccess ( target )
146235 if (
147236 targetAccess ?. member === 'request' ||
148- ( ts . isIdentifier ( target ) && requestAliases . has ( target . text ) )
237+ ( target . type === 'Identifier' &&
238+ typeof target . name === 'string' &&
239+ requestAliases . has ( target . name ) )
149240 ) {
150241 report ( node )
151242 }
152243 }
153244 }
154- ts . forEachChild ( node , visit )
245+ for ( const child of getChildNodes ( node ) ) visit ( child )
155246 }
156- visit ( sourceFile )
247+ visit ( syntaxTree . program )
157248
158249 return violations
159250}
0 commit comments