1414// 無効化: strata.config.json の { "graphql": { "enabled": false } }
1515
1616import * as path from 'node:path' ;
17- import { chainBase , readFileText , type Ctx , type Project } from '../context.ts' ;
18- import { countLines , makeLineFinder } from '../lex.ts' ;
19- import { buildFuncIndex , enclosingNodeId , topAncestorId , type FuncIndex } from './endpoints.ts' ;
17+ import { chainBase , type Ctx } from '../context.ts' ;
18+ import { countLines , makeLineFinder , matchBrace } from '../lex.ts' ;
19+ import {
20+ buildFuncIndex ,
21+ buildShortNameIndex ,
22+ enclosingNodeId ,
23+ readSource ,
24+ topAncestorId ,
25+ type FuncIndex ,
26+ } from './endpoints.ts' ;
2027
2128const ROOTS : Record < string , 'query' | 'mutation' | 'subscription' > = {
2229 Query : 'query' ,
@@ -38,18 +45,7 @@ function* typeBlocks(
3845 const re = / \b ( e x t e n d \s + ) ? ( t y p e | i n t e r f a c e ) \s + ( \w + ) ( [ ^ { } ] * ) \{ / g;
3946 for ( let m = re . exec ( sdl ) ; m ; m = re . exec ( sdl ) ) {
4047 const open = re . lastIndex - 1 ;
41- let depth = 0 ;
42- let end = - 1 ;
43- for ( let i = open ; i < sdl . length ; i ++ ) {
44- if ( sdl [ i ] === '{' ) depth ++ ;
45- else if ( sdl [ i ] === '}' ) {
46- depth -- ;
47- if ( depth === 0 ) {
48- end = i ;
49- break ;
50- }
51- }
52- }
48+ const end = matchBrace ( sdl , open ) ;
5349 if ( end < 0 ) continue ;
5450 yield {
5551 kind : ( m [ 1 ] ? 'extend ' : '' ) + m [ 3 ] ,
@@ -88,39 +84,82 @@ function entitiesOf(sdl: string): { owned: Record<string, string>; extended: Rec
8884 return { owned, extended } ;
8985}
9086
87+ /** open の位置の括弧に対応する閉じ括弧の位置。見つからなければ -1。 */
88+ function matchParen ( src : string , open : number ) : number {
89+ let depth = 0 ;
90+ for ( let i = open ; i < src . length ; i ++ ) {
91+ if ( src [ i ] === '(' ) depth ++ ;
92+ else if ( src [ i ] === ')' ) {
93+ depth -- ;
94+ if ( depth === 0 ) return i ;
95+ }
96+ }
97+ return - 1 ;
98+ }
99+
100+ /** `...Fragment` / `... on Type` を読み飛ばし、最後に読んだ位置を返す。 */
101+ function skipSpread ( body : string , start : number ) : number {
102+ let i = start ;
103+ while ( i < body . length && body [ i ] === '.' ) i ++ ;
104+ const name = / ^ \s * ( \w + ) / . exec ( body . slice ( i ) ) ;
105+ if ( ! name ) return i - 1 ;
106+ i += name [ 0 ] . length ;
107+ if ( name [ 1 ] !== 'on' ) return i - 1 ;
108+ const type = / ^ \s * \w + / . exec ( body . slice ( i ) ) ;
109+ return ( type ? i + type [ 0 ] . length : i ) - 1 ;
110+ }
111+
112+ /**
113+ * 選択セットの本文から、最上位の選択フィールド名だけを取り出す。
114+ * 引数リスト `(...)`・ディレクティブ・コメント・文字列・入れ子の選択セットは読み飛ばす。
115+ * `alias: field` は field 側を採る(スキーマに存在するのは field のため)。
116+ * 制限: ルート直下のインラインフラグメント(`... on X { ... }`)の中身は数えない。
117+ */
118+ function rootFields ( body : string ) : string [ ] {
119+ const out : string [ ] = [ ] ;
120+ let depth = 0 ;
121+ for ( let i = 0 ; i < body . length ; i ++ ) {
122+ const ch = body [ i ] ;
123+ if ( ch === '{' ) depth ++ ;
124+ else if ( ch === '}' ) depth -- ;
125+ else if ( ch === '(' ) {
126+ const close = matchParen ( body , i ) ;
127+ if ( close < 0 ) break ;
128+ i = close ;
129+ } else if ( ch === '"' ) {
130+ // 三重引用符も終端が `"` なので、次の引用符まで飛ばせば足りる
131+ while ( ++ i < body . length && body [ i ] !== '"' ) if ( body [ i ] === '\\' ) i ++ ;
132+ } else if ( ch === '#' ) {
133+ while ( i < body . length && body [ i ] !== '\n' ) i ++ ;
134+ } else if ( ch === '.' ) {
135+ i = skipSpread ( body , i ) ;
136+ } else if ( ch === '@' || ch === '$' ) {
137+ while ( i + 1 < body . length && / \w / . test ( body [ i + 1 ] ) ) i ++ ; // ディレクティブ名・変数名
138+ } else if ( / [ A - Z a - z _ ] / . test ( ch ) ) {
139+ let j = i ;
140+ while ( j < body . length && / \w / . test ( body [ j ] ) ) j ++ ;
141+ const word = body . slice ( i , j ) ;
142+ i = j - 1 ;
143+ // 直後が `:` ならエイリアス。実フィールド名は次の語なので、ここでは出さない
144+ let k = j ;
145+ while ( k < body . length && / \s / . test ( body [ k ] ) ) k ++ ;
146+ if ( body [ k ] === ':' ) continue ;
147+ if ( depth === 0 && ! [ 'on' , 'true' , 'false' , 'null' ] . includes ( word ) ) out . push ( word ) ;
148+ }
149+ }
150+ return out ;
151+ }
152+
91153/** クライアント側の操作テキストから、ルート直下の選択フィールド名を取り出す。 */
92154function selectionsOf ( op : string ) : Array < { root : 'query' | 'mutation' | 'subscription' ; name : string } > {
93155 const out : Array < { root : 'query' | 'mutation' | 'subscription' ; name : string } > = [ ] ;
94156 const re = / \b ( q u e r y | m u t a t i o n | s u b s c r i p t i o n ) \b [ ^ { } ] * \{ / g;
95157 for ( let m = re . exec ( op ) ; m ; m = re . exec ( op ) ) {
96158 const open = re . lastIndex - 1 ;
97- let depth = 0 ;
98- let end = - 1 ;
99- for ( let i = open ; i < op . length ; i ++ ) {
100- if ( op [ i ] === '{' ) depth ++ ;
101- else if ( op [ i ] === '}' ) {
102- depth -- ;
103- if ( depth === 0 ) {
104- end = i ;
105- break ;
106- }
107- }
108- }
159+ const end = matchBrace ( op , open ) ;
109160 if ( end < 0 ) continue ;
110- const body = op . slice ( open + 1 , end ) ;
111- // ルート直下(ネストの外側)の識別子だけを見る
112- let depth2 = 0 ;
113- const nameRe = / ( [ A - Z a - z _ ] \w * ) \s * ( \( | \{ | : | \n | $ ) | ( [ { } ] ) / g;
114- for ( let f = nameRe . exec ( body ) ; f ; f = nameRe . exec ( body ) ) {
115- if ( f [ 3 ] === '{' ) depth2 ++ ;
116- else if ( f [ 3 ] === '}' ) depth2 -- ;
117- else if ( depth2 === 0 && f [ 1 ] ) {
118- const nm = f [ 1 ] ;
119- if ( ! [ 'fragment' , 'on' , 'true' , 'false' , 'null' ] . includes ( nm ) ) {
120- out . push ( { root : m [ 1 ] as 'query' | 'mutation' | 'subscription' , name : nm } ) ;
121- }
122- }
123- }
161+ const root = m [ 1 ] as 'query' | 'mutation' | 'subscription' ;
162+ for ( const name of rootFields ( op . slice ( open + 1 , end ) ) ) out . push ( { root, name } ) ;
124163 re . lastIndex = end ;
125164 }
126165 return out ;
@@ -191,12 +230,8 @@ export function detectGraphql(ctx: Ctx): void {
191230 // 1) スキーマファイル(.graphql / .gql / .graphqls)
192231 for ( const project of ctx . projects ) {
193232 for ( const wsRel of project . gqlFiles ?? [ ] ) {
194- let src : string ;
195- try {
196- src = readFileText ( ctx , wsRel ) ;
197- } catch {
198- continue ;
199- }
233+ const src = readSource ( ctx , wsRel ) ;
234+ if ( src === undefined ) continue ;
200235 const dirname = path . posix . dirname ( wsRel ) ;
201236 const dir = dirname === '.' ? '' : dirname ;
202237 const base = chainBase ( ctx , project , dir ) ;
@@ -217,12 +252,8 @@ export function detectGraphql(ctx: Ctx): void {
217252 // 2) コード中のインライン SDL(Apollo の typeDefs = gql`type Query { ... }`)
218253 for ( const project of ctx . projects ) {
219254 for ( const wsRel of [ ...project . jsFiles , ...project . goFiles ] ) {
220- let src : string ;
221- try {
222- src = readFileText ( ctx , wsRel ) ;
223- } catch {
224- continue ;
225- }
255+ const src = readSource ( ctx , wsRel ) ;
256+ if ( src === undefined ) continue ;
226257 if ( ! src . includes ( 'type Query' ) && ! src . includes ( 'type Mutation' ) && ! src . includes ( 'extend type' ) ) continue ;
227258 const lineOf = makeLineFinder ( src ) ;
228259 for ( const lit of gqlLiterals ( src ) ) {
@@ -238,14 +269,7 @@ export function detectGraphql(ctx: Ctx): void {
238269 if ( fields . length === 0 && entityUsers . length === 0 ) return ;
239270
240271 // 3) リゾルバ実装 → impl 辺
241- const funcByLabel = new Map < string , string [ ] > ( ) ;
242- for ( const node of ctx . builder . nodes . values ( ) ) {
243- if ( node . kind !== 'func' ) continue ;
244- const short = node . label . includes ( '.' ) ? node . label . slice ( node . label . lastIndexOf ( '.' ) + 1 ) : node . label ;
245- const key = short . toLowerCase ( ) ;
246- if ( ! funcByLabel . has ( key ) ) funcByLabel . set ( key , [ ] ) ;
247- funcByLabel . get ( key ) ! . push ( node . id ) ;
248- }
272+ const funcByLabel = buildShortNameIndex ( ctx , true ) ;
249273 const linkImpl = ( rootKind : 'query' | 'mutation' | 'subscription' , name : string , funcId : string ) : void => {
250274 const cands = byName . get ( `${ rootKind } :${ name . toLowerCase ( ) } ` ) ?? [ ] ;
251275 if ( cands . length === 1 ) ctx . builder . addEdge ( cands [ 0 ] . id , funcId , 'impl' ) ;
@@ -254,12 +278,8 @@ export function detectGraphql(ctx: Ctx): void {
254278 for ( const project of ctx . projects ) {
255279 // Go(gqlgen): func (r *queryResolver) Users(ctx context.Context, ...)
256280 for ( const wsRel of project . goFiles ) {
257- let src : string ;
258- try {
259- src = readFileText ( ctx , wsRel ) ;
260- } catch {
261- continue ;
262- }
281+ const src = readSource ( ctx , wsRel ) ;
282+ if ( src === undefined ) continue ;
263283 if ( ! / R e s o l v e r \b / . test ( src ) ) continue ;
264284 const lineOf = makeLineFinder ( src ) ;
265285 const re = / f u n c \s * \( \s * \w + \s + \* ? ( q u e r y | m u t a t i o n | s u b s c r i p t i o n ) R e s o l v e r \s * \) \s * ( \w + ) \s * \( / gi;
@@ -271,29 +291,14 @@ export function detectGraphql(ctx: Ctx): void {
271291 }
272292 // TS/JS(Apollo): const resolvers = { Query: { users: ..., }, Mutation: { ... } }
273293 for ( const wsRel of project . jsFiles ) {
274- let src : string ;
275- try {
276- src = readFileText ( ctx , wsRel ) ;
277- } catch {
278- continue ;
279- }
294+ const src = readSource ( ctx , wsRel ) ;
295+ if ( src === undefined ) continue ;
280296 if ( ! / \b ( Q u e r y | M u t a t i o n | S u b s c r i p t i o n ) \s * : \s * \{ / . test ( src ) ) continue ;
281297 const lineOf = makeLineFinder ( src ) ;
282298 const blockRe = / \b ( Q u e r y | M u t a t i o n | S u b s c r i p t i o n ) \s * : \s * \{ / g;
283299 for ( let m = blockRe . exec ( src ) ; m ; m = blockRe . exec ( src ) ) {
284300 const open = blockRe . lastIndex - 1 ;
285- let depth = 0 ;
286- let end = - 1 ;
287- for ( let i = open ; i < src . length ; i ++ ) {
288- if ( src [ i ] === '{' ) depth ++ ;
289- else if ( src [ i ] === '}' ) {
290- depth -- ;
291- if ( depth === 0 ) {
292- end = i ;
293- break ;
294- }
295- }
296- }
301+ const end = matchBrace ( src , open ) ;
297302 if ( end < 0 ) continue ;
298303 const body = src . slice ( open + 1 , end ) ;
299304 const rootKind = ROOTS [ m [ 1 ] ] ;
@@ -319,12 +324,8 @@ export function detectGraphql(ctx: Ctx): void {
319324 // 4) クライアント操作 → graphql 辺
320325 for ( const project of ctx . projects ) {
321326 for ( const wsRel of [ ...project . jsFiles , ...project . goFiles , ...project . pyFiles ] ) {
322- let src : string ;
323- try {
324- src = readFileText ( ctx , wsRel ) ;
325- } catch {
326- continue ;
327- }
327+ const src = readSource ( ctx , wsRel ) ;
328+ if ( src === undefined ) continue ;
328329 if ( ! / \b ( q u e r y | m u t a t i o n | s u b s c r i p t i o n ) \b / . test ( src ) ) continue ;
329330 const lineOf = makeLineFinder ( src ) ;
330331 for ( const lit of gqlLiterals ( src ) ) {
0 commit comments