blob: e7ede3dfeaf9ad5a989605f8fbb2d75d6a06c0c7 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of 'analysis_options_parser.dart';
/// Walks the initial options file and its includes for one parse request.
///
/// This class owns the fixed dependencies for include traversal. Recursive
/// state is passed through [_IncludeWalkState], so each call receives the file,
/// reporter/listener pair, include chain, and first include span that apply to
/// that point in the include graph.
final class _AnalysisOptionsIncludeWalker {
final RecordingDiagnosticListener initialDiagnosticListener;
final DiagnosticReporter initialDiagnosticReporter;
final _ParsedFileNode initialParsedFileNode;
final Folder contextRoot;
final _ParsedFileSemantics Function(
_ParsedFileNode fileNode, {
required bool isInitialFile,
required Set<File> includeChainFiles,
})
fileSemantics;
_AnalysisOptionsIncludeWalker({
required this.initialDiagnosticListener,
required this.initialDiagnosticReporter,
required this.initialParsedFileNode,
required this.contextRoot,
required this.fileSemantics,
});
File get initialFile => initialParsedFileNode.file;
List<Diagnostic> walk() {
_walk(
_IncludeWalkState(
parsedFileNode: initialParsedFileNode,
diagnosticListener: initialDiagnosticListener,
diagnosticReporter: initialDiagnosticReporter,
initialIncludeSpan: null,
includeChain: const {},
),
);
return initialDiagnosticListener.diagnostics;
}
void _reportIncludedFileParseError({
required _MalformedYamlFileNode file,
required SourceSpan initialIncludeSpan,
}) {
// Report diagnostics for included option files on the `include` directive
// located in the initial options file.
var failure = file.failure;
var span = failure.span!;
initialDiagnosticReporter.report(
diag.includedFileParseError
.withArguments(
includingFilePath: file.file.path,
startOffset: span.start.offset,
endOffset: span.end.offset,
errorMessage: failure.message,
)
.atSourceSpan(initialIncludeSpan),
);
}
void _walk(_IncludeWalkState state) {
var semantics = fileSemantics(
state.parsedFileNode,
isInitialFile: state.file == initialFile,
includeChainFiles: state.includeChain.keys.toSet(),
);
for (var diagnostic in semantics.localData.diagnostics) {
state.diagnosticListener.onDiagnostic(diagnostic);
}
var legacyPlugins = semantics.localData.analyzer.legacyPlugins;
var includes = state.parsedFileNode.includeResolutions;
if (includes.isEmpty) {
legacyPlugins.reportLocalMultiplePlugins(state.diagnosticReporter);
return;
}
for (var include in includes) {
var includeSpan = include.include.node.span;
_walkInclude(
state: state,
include: include,
initialIncludeSpan: state.initialIncludeSpan ?? includeSpan,
);
}
if (semantics.includedLegacyPluginName case var includedPluginName?) {
legacyPlugins.reportMultiplePluginsWithIncluded(
state.diagnosticReporter,
includedPluginName: includedPluginName,
);
} else {
legacyPlugins.reportLocalMultiplePlugins(state.diagnosticReporter);
}
_LinterRuleDiagnostics.reportIncompatibleIncluded(
reporter: state.diagnosticReporter,
includedRules: semantics.includedLinterRules,
disabledRules: semantics.localData.localLinterRules.disabled,
);
_LinterRuleDiagnostics.reportIncompatibleWithIncluded(
reporter: state.diagnosticReporter,
localRules: semantics.localData.localLinterRules,
includedRules: semantics.includedEffectiveLinterRules,
);
}
void _walkInclude({
required _IncludeWalkState state,
required _IncludeResolution include,
required SourceSpan initialIncludeSpan,
}) {
var includeSpan = include.include.node.span;
var includeUri = include.include.uri;
switch (include) {
case _MissingInclude():
initialDiagnosticReporter.report(
diag.includeFileNotFound
.withArguments(
includedUri: includeUri,
includingFilePath: state.file.path,
contextRootPath: contextRoot.path,
)
.atSourceSpan(initialIncludeSpan),
);
return;
case _MalformedInclude(:var file):
_reportIncludedFileParseError(
file: file,
initialIncludeSpan: initialIncludeSpan,
);
return;
case _ParsedInclude(:var file):
_walkParsedInclude(
state: state,
includeSpan: includeSpan,
initialIncludeSpan: initialIncludeSpan,
includeUri: includeUri,
parsedIncludedFileNode: file,
);
return;
}
}
void _walkParsedInclude({
required _IncludeWalkState state,
required SourceSpan includeSpan,
required SourceSpan initialIncludeSpan,
required String includeUri,
required _ParsedFileNode parsedIncludedFileNode,
}) {
var includedFile = parsedIncludedFileNode.file;
if (includedFile == initialFile) {
initialDiagnosticReporter.report(
diag.recursiveIncludeFile
.withArguments(
includedUri: includeUri,
includingFilePath: state.file.path,
)
.atSourceSpan(initialIncludeSpan),
);
return;
}
assert(!state.includeChain.containsKey(state.file));
var nextIncludeChain = {...state.includeChain, state.file: includeSpan};
var spanInChain = nextIncludeChain[includedFile];
if (spanInChain != null) {
initialDiagnosticReporter.report(
diag.includedFileWarning
.withArguments(
includingFilePath: includedFile.path,
startOffset: spanInChain.start.offset,
endOffset: spanInChain.end.offset - 1,
warningMessage: 'The file includes itself recursively.',
)
.atSourceSpan(initialIncludeSpan),
);
return;
}
var diagnosticListener = _IncludedDiagnosticListener(
includedFile: includedFile,
initialDiagnosticReporter: initialDiagnosticReporter,
initialIncludeSpan: initialIncludeSpan,
);
_walk(
_IncludeWalkState(
parsedFileNode: parsedIncludedFileNode,
diagnosticListener: diagnosticListener,
diagnosticReporter: DiagnosticReporter(
diagnosticListener,
FileSource(includedFile),
),
initialIncludeSpan: initialIncludeSpan,
includeChain: nextIncludeChain,
),
);
}
}
/// Implementation of [DiagnosticListener] that converts each reported
/// [Diagnostic] into a [diag.includedFileWarning] located at the site of an
/// `include` directive.
///
/// This is used by [_AnalysisOptionsIncludeWalker] to report diagnostics
/// that occur in included options files.
final class _IncludedDiagnosticListener implements DiagnosticListener {
/// The included options file whose diagnostics are being translated.
final File includedFile;
/// The [DiagnosticReporter] for the initial source file (the one containing
/// the first `include` in the chain of `include`s that's currently being
/// processed).
final DiagnosticReporter initialDiagnosticReporter;
/// The [SourceSpan] of the first `include` in the chain of `include`s that's
/// currently being processed.
final SourceSpan initialIncludeSpan;
_IncludedDiagnosticListener({
required this.includedFile,
required this.initialDiagnosticReporter,
required this.initialIncludeSpan,
});
@override
void onDiagnostic(Diagnostic diagnostic) {
initialDiagnosticReporter.report(
diag.includedFileWarning
.withArguments(
includingFilePath: includedFile.path,
startOffset: diagnostic.offset,
endOffset: diagnostic.offset + diagnostic.length - 1,
warningMessage: diagnostic.message,
)
.atSourceSpan(initialIncludeSpan),
);
}
}
/// State for one recursive include-walk call.
final class _IncludeWalkState {
final _ParsedFileNode parsedFileNode;
final DiagnosticListener diagnosticListener;
final DiagnosticReporter diagnosticReporter;
final SourceSpan? initialIncludeSpan;
final Map<File, SourceSpan> includeChain;
_IncludeWalkState({
required this.parsedFileNode,
required this.diagnosticListener,
required this.diagnosticReporter,
required this.initialIncludeSpan,
required this.includeChain,
});
File get file => parsedFileNode.file;
}