@@ -263,6 +263,7 @@ internal static int Run(
263263 "workspace" => WorkspaceCommandRunner . Run ( subArgs , jsonOptions ) ,
264264 "db" => DbCommandRunner . RunIntegrityCheck ( subArgs , jsonOptions ) ,
265265 "report" => ReportCommandRunner . Run ( subArgs , jsonOptions , appVersion ) ,
266+ "test-extractor" => RunTestExtractor ( subArgs , jsonOptions ) ,
266267 _ when IsProjectPathArg ( commandName )
267268 => IndexCommandRunner . Run ( args , jsonOptions ) ,
268269 _ => ShowError ( args , $ "Unknown command: { commandName } ")
@@ -332,6 +333,74 @@ private static bool IsWindowsDrivePath(string arg) =>
332333 && arg [ 1 ] == ':'
333334 && ( ( arg [ 0 ] >= 'A' && arg [ 0 ] <= 'Z' ) || ( arg [ 0 ] >= 'a' && arg [ 0 ] <= 'z' ) ) ;
334335
336+ private static int RunTestExtractor ( string [ ] args , JsonSerializerOptions jsonOptions )
337+ {
338+ string ? language = null ;
339+ string ? file = null ;
340+ string ? expect = null ;
341+ var json = false ;
342+ for ( var i = 0 ; i < args . Length ; i ++ )
343+ {
344+ var arg = args [ i ] ;
345+ if ( TryConsumeInlineOrNext ( args , ref i , arg , "--language" , out var value ) )
346+ language = value ;
347+ else if ( TryConsumeInlineOrNext ( args , ref i , arg , "--file" , out value ) )
348+ file = value ;
349+ else if ( TryConsumeInlineOrNext ( args , ref i , arg , "--expect-symbols" , out value ) || TryConsumeInlineOrNext ( args , ref i , arg , "--expect" , out value ) )
350+ expect = value ;
351+ else if ( arg == "--json" )
352+ json = true ;
353+ else
354+ return CommandErrorWriter . Write ( $ "Unknown test-extractor argument: { arg } ", CommandExitCodes . InvalidArgument , "use --language <lang> --file <path> [--expect-symbols <json>] [--json]." ) ;
355+ }
356+
357+ if ( string . IsNullOrWhiteSpace ( language ) || string . IsNullOrWhiteSpace ( file ) )
358+ return CommandErrorWriter . Write ( "test-extractor requires --language and --file." , CommandExitCodes . InvalidArgument , "use --language <lang> --file <path> [--expect-symbols <json>] [--json]." ) ;
359+ if ( ! File . Exists ( file ) )
360+ return CommandErrorWriter . Write ( $ "File not found: { file } ", CommandExitCodes . NotFound ) ;
361+
362+ var source = File . ReadAllText ( file ) ;
363+ var symbols = Indexer . SymbolExtractor . Extract ( 1 , language , source , file ) ;
364+ if ( expect != null )
365+ {
366+ var expected = File . ReadAllText ( expect ) ;
367+ var actual = JsonSerializer . Serialize ( symbols ) ;
368+ if ( ! JsonEquivalent ( expected , actual ) )
369+ {
370+ Console . Error . WriteLine ( "Expected symbols did not match extracted symbols." ) ;
371+ Console . Error . WriteLine ( actual ) ;
372+ return CommandExitCodes . InvalidArgument ;
373+ }
374+ }
375+
376+ if ( json || expect == null )
377+ Console . WriteLine ( JsonSerializer . Serialize ( symbols ) ) ;
378+ return CommandExitCodes . Success ;
379+ }
380+
381+ private static bool TryConsumeInlineOrNext ( string [ ] args , ref int index , string arg , string flag , out string value )
382+ {
383+ value = string . Empty ;
384+ if ( arg . StartsWith ( flag + "=" , StringComparison . Ordinal ) )
385+ {
386+ value = arg [ ( flag . Length + 1 ) ..] ;
387+ return true ;
388+ }
389+
390+ if ( arg != flag || index + 1 >= args . Length )
391+ return false ;
392+
393+ value = args [ ++ index ] ;
394+ return true ;
395+ }
396+
397+ private static bool JsonEquivalent ( string expected , string actual )
398+ {
399+ using var expectedDoc = JsonDocument . Parse ( expected ) ;
400+ using var actualDoc = JsonDocument . Parse ( actual ) ;
401+ return JsonSerializer . Serialize ( expectedDoc . RootElement ) == JsonSerializer . Serialize ( actualDoc . RootElement ) ;
402+ }
403+
335404 internal static void EnsureRedirectedStdoutUsesUtf8 ( )
336405 {
337406 if ( ! Console . IsOutputRedirected || Console . Out is StringWriter || Console . Out . GetType ( ) . Assembly != typeof ( Console ) . Assembly )
0 commit comments