analyzeFile method
Analyzes a file on disk by file path and 1-based line bounds.
Implementation
Future<DataFlowResult> analyzeFile({
required String filePath,
required int startLine,
required int endLine,
String methodName = '_extracted',
}) async {
final absPath = p.normalize(p.absolute(filePath));
final file = File(absPath);
if (!file.existsSync()) {
throw FileSystemException('Target file does not exist', filePath);
}
final helper = AnalysisContextHelper(
includedPaths: [absPath],
sdkPath: sdkPath,
);
final unitResult = await helper.getRequiredResolvedUnit(absPath);
final lineInfo = unitResult.lineInfo;
if (startLine < 1 || endLine < startLine) {
throw FormatException(
'Invalid line bounds: startLine ($startLine) must be >= 1 and '
'<= endLine ($endLine)',
);
}
final totalLines = lineInfo.lineCount;
if (startLine > totalLines) {
throw FormatException(
'Start line $startLine exceeds total file lines ($totalLines)',
);
}
final startOffset = lineInfo.getOffsetOfLine(startLine - 1);
final endOffset = endLine >= totalLines
? unitResult.unit.end
: lineInfo.getOffsetOfLine(endLine) - 1;
final locator = _EnclosingDeclarationVisitor(
startOffset,
endLine,
lineInfo,
);
unitResult.unit.accept(locator);
final enclosingNode = locator.enclosing;
if (enclosingNode == null) {
throw FormatException(
'Target line range $startLine-$endLine does not fall entirely within '
'a single function or method declaration.',
);
}
final enclosingName = switch (enclosingNode) {
FunctionDeclaration(:final name) ||
MethodDeclaration(:final name) => name.lexeme,
ConstructorDeclaration(:final name) => name?.lexeme ?? 'new',
_ => 'unknown',
};
// 1. Traverse within slice for inputs, mutations, escapes, and async
final inBlockVisitor = InBlockVisitor(
sliceStartOffset: startOffset,
sliceEndOffset: endOffset,
lineInfo: lineInfo,
);
enclosingNode.accept(inBlockVisitor);
// 2. Traverse after slice for liveness analysis (outputs)
final loopCollector = _EnclosingLoopCollector(startOffset, endOffset);
enclosingNode.accept(loopCollector);
final postBlockVisitor = PostBlockVisitor(
sliceStartOffset: startOffset,
sliceEndOffset: endOffset,
lineInfo: lineInfo,
internalDeclarations: inBlockVisitor.internalDeclarations,
mutations: inBlockVisitor.mutations,
enclosingLoopSpans: loopCollector.spans,
);
enclosingNode.accept(postBlockVisitor);
final inputList = inBlockVisitor.inputs.values.toList();
final mutationList = inBlockVisitor.mutations.values.toList();
final outputList = postBlockVisitor.liveOutputs.values.toList();
final escapes = inBlockVisitor.escapes;
final typeParams = _extractTypeParams(enclosingNode, inputList, outputList);
final signature = synthesizer.synthesize(
inputs: inputList,
outputs: outputList,
typeParameters: typeParams,
methodName: methodName,
isAsync: inBlockVisitor.hasAwait,
);
final (:enclosingScore, :sliceScoreInPlace, :sliceScoreAtRoot) =
_computeComplexityImpact(enclosingNode, startOffset, endOffset);
final warnings = _buildExtractionWarnings(
inputCount: inputList.length,
sliceLineCount: endLine - startLine + 1,
enclosingScore: enclosingScore,
sliceScoreInPlace: sliceScoreInPlace,
);
return DataFlowResult(
filePath: filePath,
startLine: startLine,
endLine: endLine,
enclosingDeclaration: enclosingName,
inputs: inputList,
mutations: mutationList,
outputs: outputList,
escapes: escapes,
suggestedSignature: signature,
isCleanlyExtractable: escapes.isEmpty,
enclosingScore: enclosingScore,
sliceScoreInPlace: sliceScoreInPlace,
sliceScoreAtRoot: sliceScoreAtRoot,
extractionWarnings: warnings,
);
}