scan method

  1. @override
Future<ScannerResult> scan(
  1. ProjectContext context
)
override

Executes the scan and returns a ScannerResult.

Implementation

@override
Future<ScannerResult> scan(ProjectContext context) async {
  context.logger.startSpinner('Scanning Code Quality...');
  final issues = <Issue>[];

  final dartFiles = FileUtils.getDartFiles(context.directory);
  final parser = AstParser(context);

  final stringLiterals = <String, List<String>>{};
  int totalClasses = 0;
  int totalMethods = 0;

  // 1. Run dart analyze to catch standard lints (unused imports, etc.)
  try {
    final result = await Process.run('dart', [
      'analyze',
      '--format=machine',
    ], workingDirectory: context.directory);
    // dart analyze exits with non-zero if issues are found, which is expected.
    final output = '${result.stdout}\n${result.stderr}';
    final lines = output.split('\n');
    for (final line in lines) {
      if (line.trim().isEmpty) continue;
      final parts = line.split('|');
      if (parts.length >= 8) {
        final severityStr = parts[0];

        final errorCode = parts[2];
        final filePath = parts[3];
        final lineNum = int.tryParse(parts[4]) ?? 0;
        final message = parts[7];

        IssueSeverity severity = IssueSeverity.low;
        if (severityStr == 'ERROR') severity = IssueSeverity.critical;
        if (severityStr == 'WARNING') severity = IssueSeverity.high;
        if (severityStr == 'INFO') severity = IssueSeverity.medium;

        issues.add(
          Issue(
            title: errorCode,
            description: message,
            category: 'Code Quality',
            severity: severity,
            file: filePath,
            line: lineNum,
            suggestion:
                'Run `flutter_doctor_pro fix` to attempt automatic resolution.',
          ),
        );
      }
    }
  } catch (e) {
    context.logger.verboseLog('Failed to run dart analyze: $e');
  }

  // 2. Run AST parser for structural issues
  for (final file in dartFiles) {
    final content = await file.readAsString();
    final lines = content.split('\n');

    // Fast check for TODO/FIXME
    for (int i = 0; i < lines.length; i++) {
      final line = lines[i];
      if (line.contains('TODO:') ||
          line.contains('TODO ') ||
          line.contains('TODO(')) {
        issues.add(
          Issue(
            title: 'TODO Comment',
            description: 'Found TODO comment: ${line.trim()}',
            category: 'Code Quality',
            severity: IssueSeverity.low,
            file: file.path,
            line: i + 1,
            suggestion: 'Resolve the TODO.',
          ),
        );
      }
    }

    final unit = parser.parseFile(file.path, content);
    if (unit == null) continue;

    final visitor = _QualityVisitor(file.path, issues, stringLiterals);
    unit.visitChildren(visitor);

    totalClasses += visitor.classCount;
    totalMethods += visitor.methodCount;
  }

  // Process duplicate strings
  stringLiterals.forEach((str, paths) {
    if (paths.length >= 3) {
      issues.add(
        Issue(
          title: 'Duplicate String',
          description: 'String "$str" is duplicated ${paths.length} times.',
          category: 'Code Quality',
          severity: IssueSeverity.low,
          suggestion: 'Extract to a constant or localization.',
        ),
      );
    }
  });

  context.logger.stopSpinner();
  return ScannerResult(
    issues: issues,
    metrics: {'total_classes': totalClasses, 'total_methods': totalMethods},
  );
}