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,
),
);
}