From e1d226d90454bdcdfbf5dbc18b29fabfb4bd41e7 Mon Sep 17 00:00:00 2001 From: ivkond Date: Thu, 9 Apr 2026 18:12:52 +0300 Subject: [PATCH] feat(group): expand grpc extraction and proto resolution coverage --- .../core/group/extractors/grpc-extractor.ts | 10 ++++-- .../test/unit/group/grpc-extractor.test.ts | 35 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/gitnexus/src/core/group/extractors/grpc-extractor.ts b/gitnexus/src/core/group/extractors/grpc-extractor.ts index c5fe11167..fdaf59953 100644 --- a/gitnexus/src/core/group/extractors/grpc-extractor.ts +++ b/gitnexus/src/core/group/extractors/grpc-extractor.ts @@ -595,13 +595,19 @@ export class GrpcExtractor implements ContractExtractor { ); }; - const getServiceRe = /\.getService(?:<[^>]+>)?\s*\(\s*['"](\w+)['"]\s*\)/g; + const grpcClientDecoratorRe = + /@GrpcClient\s*\([^)]*\)\s*(?:private|protected|public)?\s*(?:readonly\s+)?\w+[!?]?\s*:\s*(\w+Service)Client\b/g; let match: RegExpExecArray | null; + while ((match = grpcClientDecoratorRe.exec(content)) !== null) { + pushConsumer(match[1], `${match[1]}Client`, 'ts_grpc_client_decorator'); + } + + const getServiceRe = /\.getService(?:<[^>]+>)?\s*\(\s*['"](\w+)['"]\s*\)/g; while ((match = getServiceRe.exec(content)) !== null) { pushConsumer(match[1], `${match[1]}Client`, 'ts_client_grpc_get_service'); } - const clientCtorRe = /new\s+(\w+)Client\s*\(/g; + const clientCtorRe = /new\s+(\w+Service)Client\s*\(/g; while ((match = clientCtorRe.exec(content)) !== null) { pushConsumer(match[1], `${match[1]}Client`, 'ts_generated_client'); } diff --git a/gitnexus/test/unit/group/grpc-extractor.test.ts b/gitnexus/test/unit/group/grpc-extractor.test.ts index 421093e15..dc555592a 100644 --- a/gitnexus/test/unit/group/grpc-extractor.test.ts +++ b/gitnexus/test/unit/group/grpc-extractor.test.ts @@ -380,7 +380,7 @@ export class AuthController { expect(providers[0].confidence).toBe(0.8); }); - it('test_extract_ts_grpc_client_decorator_and_getService_returns_consumer', async () => { + it('test_extract_ts_grpc_client_decorator_returns_consumer', async () => { writeFile( 'proto/auth.proto', `syntax = "proto3"; @@ -391,15 +391,12 @@ service AuthService { ); writeFile( 'src/auth.client.ts', - `import { ClientGrpc, GrpcClient } from '@nestjs/microservices'; + `import { GrpcClient } from '@nestjs/microservices'; +import type { AuthServiceClient } from './generated/auth'; export class AuthGateway { - @GrpcClient('AUTH_PACKAGE') - private readonly client!: ClientGrpc; - - onModuleInit(): void { - this.client.getService('AuthService'); - } + @GrpcClient({ package: 'auth.v1', protoPath: 'proto/auth.proto' }) + private readonly authClient!: AuthServiceClient; }`, ); @@ -459,6 +456,28 @@ export const authClient = new AuthServiceClient('localhost:50051', credentials.c expect(consumers[0].contractId).toBe('grpc::auth.v1.AuthService/*'); }); + it('test_extract_ts_non_service_client_constructor_is_ignored', async () => { + writeFile( + 'proto/auth.proto', + `syntax = "proto3"; +package auth.v1; +service AuthService { + rpc Login (LoginRequest) returns (LoginResponse); +}`, + ); + writeFile( + 'src/auth.client.ts', + `import { AuthClient } from './generated/auth'; + +export const authClient = new AuthClient('localhost:50051');`, + ); + + const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir)); + const consumers = contracts.filter((c) => c.role === 'consumer'); + + expect(consumers).toHaveLength(0); + }); + it('test_extract_ts_loadPackageDefinition_constructor_returns_consumer', async () => { writeFile( 'proto/auth.proto',