| // 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; |
| } |