extract method

  1. @override
Future<IrAdapterOutput> extract(
  1. String rootPath, {
  2. List<String> roots = const ['lib'],
})

Implementation

@override
Future<IrAdapterOutput> extract(
  String rootPath, {
  List<String> roots = const ['lib'],
}) async {
  late final String root;
  try {
    root = Directory(rootPath).resolveSymbolicLinksSync();
  } catch (_) {
    root = Directory(rootPath).absolute.path;
  }
  final symbols = <ExtractedSymbol>[];
  final errors = <String>[];
  final graphNodes = <String, IrNode>{};
  final graphEdges = <IrEdge>[];
  var graphCompleteness = const GraphCompleteness(
    routeRegistration: CompletenessValue.notApplicable,
    middlewareOrder: CompletenessValue.notApplicable,
    failureFlow: CompletenessValue.notApplicable,
    logFlow: CompletenessValue.notApplicable,
    dynamicRegistration: CompletenessValue.notApplicable,
    externalVisibility: CompletenessValue.notApplicable,
  );
  final packageName = _packageName(root);
  final sourceDirectories = roots
      .map(
        (path) => Directory(
          path == '.' || path.isEmpty
              ? root
              : '$root${Platform.pathSeparator}$path',
        ),
      )
      .where((directory) => directory.existsSync())
      .toList();
  if (sourceDirectories.isEmpty) {
    return IrAdapterOutput(
      adapter: adapterInfo,
      completeness: const IrAdapterCompleteness(),
      symbols: const [],
      inputDigest: _computeDigest(root),
      diagnostics: const [],
      packageName: _packageName(root),
      packageRoot: root,
    );
  }

  final dartFiles =
      sourceDirectories
          .expand(
            (directory) =>
                directory.listSync(recursive: true, followLinks: false),
          )
          .whereType<File>()
          .where((f) => f.path.endsWith('.dart'))
          .map((f) => f.absolute.resolveSymbolicLinksSync())
          .toSet()
          .toList()
        ..sort();

  final collection = AnalysisContextCollection(
    includedPaths: [root],
    sdkPath: resolveAnalyzerSdkPath(),
  );
  try {
    for (final file in dartFiles) {
      try {
        final result = await collection
            .contextFor(file)
            .currentSession
            .getResolvedUnit(file);
        if (result is! ResolvedUnitResult) {
          errors.add('$file: analyzer did not return a resolved unit');
          continue;
        }
        final severeErrors = result.diagnostics
            .where(
              (error) =>
                  error.diagnosticCode.severity ==
                  analyzer_error.DiagnosticSeverity.ERROR,
            )
            .toList();
        if (severeErrors.isNotEmpty) {
          for (final error in severeErrors) {
            errors.add('$file:${error.offset}: ${error.message}');
          }
          continue;
        }
        result.unit.accept(
          _ResolvedVisitor(
            packageName: packageName,
            file: file,
            lineInfo: result.lineInfo,
            symbols: symbols,
            errors: errors,
            graphNodes: graphNodes,
            graphEdges: graphEdges,
          ),
        );
      } catch (e) {
        errors.add('$file: resolution failed: $e');
      }
    }
  } finally {
    await collection.dispose();
  }

  // Registration topology and annotations are collected by separate AST
  // visits. Join an observed registered controller to its resolved
  // @ImplementsRequirement symbol only after every unit has been visited.
  for (final entry in graphNodes.entries.toList()) {
    final node = entry.value;
    if (node.kind != NodeKind.implementation ||
        !node.id.startsWith('implementation:')) {
      continue;
    }
    final typeName = node.id.substring('implementation:'.length);
    final implementation = symbols
        .where(
          (symbol) =>
              symbol.kind == 'requirementBoundary' &&
              symbol.symbolId.endsWith('#$typeName'),
        )
        .toList();
    if (implementation.length != 1) {
      errors.add(
        '${node.id}: registered controller must have exactly one '
        '@ImplementsRequirement declaration',
      );
      continue;
    }
    graphNodes[entry.key] = IrNode(
      id: node.id,
      kind: node.kind,
      target: implementation.single.target ?? node.target,
      role: node.role,
      variant: implementation.single.variant,
      slot: implementation.single.slot,
      source: node.source,
      properties: {
        ...node.properties,
        'requirementIds': implementation.single.requirementIds,
      },
    );
  }
  for (final entry in graphNodes.entries.toList()) {
    final node = entry.value;
    if (node.kind != NodeKind.provider || !node.id.startsWith('provider:')) {
      continue;
    }
    final typeName = node.id.substring('provider:'.length);
    final provider = symbols
        .where(
          (symbol) =>
              symbol.kind == 'controlProvider' &&
              symbol.symbolId.endsWith('#$typeName'),
        )
        .toList();
    if (provider.length != 1 || provider.single.controlIds.length != 1) {
      errors.add(
        '${node.id}: registered provider must have exactly one resolved '
        '@ProvidesControl declaration',
      );
      continue;
    }
    graphNodes[entry.key] = IrNode(
      id: node.id,
      kind: node.kind,
      target: provider.single.target ?? node.target,
      role: node.role,
      variant: provider.single.variant,
      slot: provider.single.slot,
      source: node.source,
      properties: {
        ...node.properties,
        'controlId': provider.single.controlIds.single,
        'providerKind': provider.single.providerKind,
        'layer': provider.single.layer,
      },
    );
  }

  symbols.sort((a, b) {
    final left = '${a.source.uri}:${a.source.offset}:${a.kind}:${a.symbolId}';
    final right =
        '${b.source.uri}:${b.source.offset}:${b.kind}:${b.symbolId}';
    return left.compareTo(right);
  });
  if (graphNodes.isNotEmpty) {
    final incomplete = errors.any((error) => error.contains('dynamic'));
    graphCompleteness = GraphCompleteness(
      routeRegistration: incomplete
          ? CompletenessValue.indeterminate
          : CompletenessValue.complete,
      middlewareOrder: incomplete
          ? CompletenessValue.indeterminate
          : CompletenessValue.complete,
      failureFlow: incomplete
          ? CompletenessValue.indeterminate
          : CompletenessValue.complete,
      logFlow: incomplete
          ? CompletenessValue.indeterminate
          : CompletenessValue.complete,
      dynamicRegistration: incomplete
          ? CompletenessValue.indeterminate
          : CompletenessValue.complete,
      externalVisibility: CompletenessValue.notApplicable,
    );
    // Runtime registrations are first-class provider candidates even when
    // the implementation class has no annotation.  They are emitted from
    // the same resolved constructor expressions as the graph, so mapping
    // cannot silently accept a decorative provider declaration.
    for (final node in graphNodes.values) {
      if (node.kind != NodeKind.provider) continue;
      final control = node.properties['controlId'];
      if (control is! String || control.isEmpty) continue;
      final sourceUri =
          node.properties['sourceUri']?.toString() ??
          'package:$packageName/unknown.dart';
      final line = (node.properties['sourceLine'] as num?)?.toInt() ?? 1;
      final providerKind = node.properties['providerKind']?.toString();
      if (providerKind == null || providerKind.isEmpty) {
        errors.add(
          '${node.id}: registered provider must resolve a declared '
          '@ProvidesControl provider kind',
        );
        continue;
      }
      symbols.add(
        ExtractedSymbol(
          kind: 'controlProvider',
          role: 'provider',
          symbolId: '$sourceUri#${node.id}',
          controlIds: [control],
          providerKind: providerKind,
          target: 'backend',
          source: ExtractedSourceLocation(
            uri: sourceUri,
            offset: 0,
            length: 0,
            line: line,
            column: 1,
          ),
        ),
      );
    }
  }
  symbols.sort((a, b) {
    final left = '${a.source.uri}:${a.source.offset}:${a.kind}:${a.symbolId}';
    final right =
        '${b.source.uri}:${b.source.offset}:${b.kind}:${b.symbolId}';
    return left.compareTo(right);
  });
  return IrAdapterOutput(
    adapter: adapterInfo,
    completeness: IrAdapterCompleteness(
      annotationTargets: errors.isEmpty
          ? CompletenessValue.complete
          : CompletenessValue.indeterminate,
      graph: graphCompleteness,
    ),
    symbols: symbols,
    inputDigest: _computeDigest(root),
    diagnostics: errors
        .map(
          (message) => IrDiagnostic(
            code: 'DART-EXTRACT-001',
            message: message,
            severity: IrDiagnosticSeverity.error,
          ),
        )
        .toList(),
    packageName: packageName,
    packageRoot: root,
    graph: graphNodes.isEmpty
        ? null
        : IrGraph(
            nodes: graphNodes.values.toList(),
            edges: graphEdges,
            completeness: graphCompleteness,
          ),
  );
}