run method

Future<void> run()

Implementation

Future<void> run() async {
  final parser = ArgParser()
    ..addCommand('analyze')
    ..addCommand('affected')
    ..addCommand('run');

  final affectedCmd = parser.commands['affected'];
  if (affectedCmd != null) {
    affectedCmd.addFlag('json', negatable: false, help: 'Output result in JSON format');
    affectedCmd.addFlag('conservative', negatable: false, help: 'Conservative mode for fallback safety');
    affectedCmd.addFlag('symbol-level', negatable: false, help: 'Use symbol-level dependency analysis');
    affectedCmd.addFlag('explain', negatable: false, help: 'Explain symbol impact paths');
  }

  final runCmd = parser.commands['run'];
  if (runCmd != null) {
    runCmd.addFlag('json', negatable: false, help: 'Output result in JSON format');
    runCmd.addFlag('all', negatable: false, help: 'Always run full test suite');
    runCmd.addFlag('conservative', negatable: false, help: 'Conservative mode for fallback safety');
    runCmd.addFlag('symbol-level', negatable: false, help: 'Use symbol-level dependency analysis');
  }

  ArgResults results;
  try {
    results = parser.parse(args);
  } catch (e) {
    _printUsage(parser);
    exit(64);
  }

  final command = results.command;
  if (command == null) {
    _printUsage(parser);
    return;
  }

  final cache = AnalysisCache(projectRoot: currentDirectory)..load();
  final projectAnalyzer = ProjectAnalyzer(
    projectRoot: currentDirectory,
    cache: cache,
  );

  late ProjectAnalysisResult analysis;
  bool hasAnalysisErrors = false;
  String? analysisErrorReason;

  try {
    analysis = projectAnalyzer.analyze();
    cache.save();
    if (analysis.warnings.isNotEmpty) {
      hasAnalysisErrors = true;
      analysisErrorReason = 'syntax_error';
    }
  } catch (e) {
    hasAnalysisErrors = true;
    analysisErrorReason = 'analysis_failed';
  }

  switch (command.name) {
    case 'analyze':
      for (final warning in analysis.warnings) {
        print(warning);
      }
      CliOutput.printAnalysis(
        fileCount: analysis.sourceFiles.length,
        testCount: analysis.testFiles.length,
        dependencyCount: analysis.graph.totalEdges,
      );
      break;

    case 'affected':
      final isConservative = command['conservative'] == true;
      final useSymbolLevel = command['symbol-level'] == true;
      final explain = command['explain'] == true;

      final gitDetector = GitChangeDetector(projectRoot: currentDirectory);
      final gitResult = await gitDetector.detectChanges();

      final fileImpactAnalyzer = ImpactAnalyzer(
        graph: analysis.graph,
        discoveredTests: analysis.testFiles,
      );

      late final impactResult;

      if (useSymbolLevel) {
        final symbolAnalyzer = SymbolAnalyzer(
          projectRoot: currentDirectory,
          packageName: analysis.packageName,
        );
        final symbolResult = await symbolAnalyzer.analyzeProject();

        final symbolImpactAnalyzer = SymbolImpactAnalyzer(
          symbolGraph: symbolResult.graph,
          discoveredTests: analysis.testFiles,
          fallbackFileAnalyzer: fileImpactAnalyzer,
        );

        impactResult = symbolImpactAnalyzer.analyzeSymbolImpact(
          changedFiles: gitResult.changedFiles,
          isGitRepo: gitResult.isGitRepository,
          gitRequiresFallback: gitResult.requiresFallback,
          gitFallbackReason: gitResult.fallbackReason,
          isConservative: isConservative,
          hasAnalysisErrors: hasAnalysisErrors,
          analysisErrorReason: analysisErrorReason,
        );
      } else {
        impactResult = fileImpactAnalyzer.analyzeImpact(
          changedFiles: gitResult.changedFiles,
          isGitRepo: gitResult.isGitRepository,
          gitRequiresFallback: gitResult.requiresFallback,
          gitFallbackReason: gitResult.fallbackReason,
          isConservative: isConservative,
          hasAnalysisErrors: hasAnalysisErrors,
          analysisErrorReason: analysisErrorReason,
        );
      }

      if (command['json'] == true) {
        CliOutput.printJson(impactResult);
      } else {
        CliOutput.printAffected(impactResult, warningMessage: gitResult.warningMessage, explain: explain);
      }
      break;

    case 'run':
      final forceAll = command['all'] == true;
      final isConservative = command['conservative'] == true;
      final useSymbolLevel = command['symbol-level'] == true;

      final gitDetector = GitChangeDetector(projectRoot: currentDirectory);
      final gitResult = await gitDetector.detectChanges();

      final fileImpactAnalyzer = ImpactAnalyzer(
        graph: analysis.graph,
        discoveredTests: analysis.testFiles,
      );

      late final impactResult;

      if (useSymbolLevel) {
        final symbolAnalyzer = SymbolAnalyzer(
          projectRoot: currentDirectory,
          packageName: analysis.packageName,
        );
        final symbolResult = await symbolAnalyzer.analyzeProject();

        final symbolImpactAnalyzer = SymbolImpactAnalyzer(
          symbolGraph: symbolResult.graph,
          discoveredTests: analysis.testFiles,
          fallbackFileAnalyzer: fileImpactAnalyzer,
        );

        impactResult = symbolImpactAnalyzer.analyzeSymbolImpact(
          changedFiles: gitResult.changedFiles,
          isGitRepo: gitResult.isGitRepository,
          gitRequiresFallback: gitResult.requiresFallback,
          gitFallbackReason: gitResult.fallbackReason,
          isConservative: isConservative,
          forceAll: forceAll,
          hasAnalysisErrors: hasAnalysisErrors,
          analysisErrorReason: analysisErrorReason,
        );
      } else {
        impactResult = fileImpactAnalyzer.analyzeImpact(
          changedFiles: gitResult.changedFiles,
          isGitRepo: gitResult.isGitRepository,
          gitRequiresFallback: gitResult.requiresFallback,
          gitFallbackReason: gitResult.fallbackReason,
          isConservative: isConservative,
          forceAll: forceAll,
          hasAnalysisErrors: hasAnalysisErrors,
          analysisErrorReason: analysisErrorReason,
        );
      }

      if (command['json'] == true) {
        CliOutput.printJson(impactResult);
      }

      if (impactResult.affectedTests.isEmpty && !impactResult.isFallback) {
        print('No affected tests detected.');
        return;
      }

      if (impactResult.isFallback) {
        print('\u26a0 ' + (impactResult.fallbackExplanation ?? ''));
        print('Running ALL ' + impactResult.totalTests.toString() + ' test(s)...\n');
      } else {
        print('Running ' + impactResult.affectedCount.toString() + ' affected test(s)...\n');
      }

      final runner = TestRunner(
        projectRoot: currentDirectory,
        isFlutterProject: analysis.isFlutterProject,
      );

      final testPathsToRun = impactResult.isFallback
          ? analysis.testFiles.map((t) => t.path).toList()
          : impactResult.affectedTests.map((a) => a.testPath).toList();

      final testResult = await runner.runTests(testPathsToRun);

      print(testResult.stdout);
      if (testResult.stderr.isNotEmpty) {
        stderr.write(testResult.stderr);
      }
      exit(testResult.exitCode);

    default:
      _printUsage(parser);
  }
}