Flutter 3.47's Auto-Migration Silently Corrupts Analysis Configs
theme: smartblue
Flutter 3.47 has just been released and immediately brought two notorious issues: #191056 and #191131. Coincidentally, this problem was discussed with Alex at Google's APAC Summit in Shanghai. Both issues stem from the same change: the newly introduced AnalysisOptionsMigration in Flutter 3.47.
This change was originally intended to solve a very small problem but ended up creating two design flaws:
- When judging the configuration, it did not understand the
include:inheritance relationship ofanalysis_options.yaml. - When deciding which directories to exclude, it hardcoded all six of Flutter's platform directories.
The former problem causes repeated rewriting of configuration files in monorepos, while the latter can even silently kick genuine Dart Web code out of the Analyzer's inspection scope.
The story began with #187728. The issue was that scenarios like FlutterFire with Swift Package Manager would leave Dart files in build/, and the Analysis Server might continue scanning these generated contents, producing a large number of meaningless analysis errors.
This problem indeed needed solving. The build/ directory and various platform project directories generally do not contain Dart code that users need to analyze, so excluding these directories by default is very reasonable. Then PR #187940 did two things:
- Modified the
flutter createtemplate so that new projects'analysis_options.yamlwould include by default:
analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
- To accommodate existing Flutter projects, it also added
AnalysisOptionsMigration, which automatically inserts these configurations.
The problem is that this migration was directly hooked into FlutterProject.ensureReadyForPlatformSpecificTooling(), meaning it doesn't run only under a specific "upgrade project" command. Daily commands like flutter pub get, flutter analyze, flutter run, and flutter build all trigger it.
As a result, the problem emerged. A migration that automatically modifies user repository files should theoretically be conservative enough in its judgments, but the first version's implementation was very crude:
- Read the
analysis_options.yamlin the current project root directory. - Parse the YAML.
- Directly check
root['analyzer']['exclude']in the current file. - If there is no
analyzer, noexclude, or if any of the seven specified directories is missing, it considers the project needs migration and writes the missing items back to the current file.
This led to issue #191056. A very common way to write Dart's analysis_options.yaml is to centralize the configuration, for example:
# analysis_options.yaml
include: package:company_lints/flutter.yaml
The actual configuration might be inside company_lints:
analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
For the Dart Analyzer, these exclusions are already in effect. For instance:
- Placing an obvious type error into
android/probe.dart,dart analyzewill not report it. - Placing the same error into
lib/probe.dart, the Analyzer will correctly report the error.
But the problem is that Flutter 3.47's migration did not parse include: at all. It only saw that the current file did not literally contain:
analyzer:
exclude:
And then entered the "not yet migrated" case, appending a completely identical exclusion to the current file. After Flutter ran the command once more, it became:
include: package:company_lints/flutter.yaml
analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
Even if you manually deleted it, running flutter pub get or flutter analyze again would cause the migration to add it back. After massive feedback, the official team urgently fixed this in PR #191082:
The new
_collectExcludes()reads the current file's ownanalyzer.exclude, then recursively resolvesinclude:. Relative paths can continue to be found from the current file's directory,package:URIs are resolved to real files viaPackageConfig, and canonical paths are recorded during recursion to avoid infinite loops from two configuration files including each other. Finally, protection has been added for format errors, unreadable files, and abnormal exclude types.
But then another problem arose. Just as #191082 solved the issue of "how to read existing configurations," #191131 immediately exposed the other half: What gives Flutter the right to assume that all projects should exclude these seven directories?
The first version of AnalysisOptionsMigration's exclusion was a fixed list:
const excludesToExclude = <String>[
'build/**',
'android/**',
'ios/**',
'web/**',
'windows/**',
'macos/**',
'linux/**',
];
It has nothing to do with which platforms the project actually enables. A Flutter project created only for Android and iOS would still get web/**, windows/**, macos/**, and linux/**. Even if you manually delete these configurations, the next Flutter command will add them back, meaning the pollution problem persists.
Especially if a project was created using Dart's own dart create -t web webrepro, the genuine Dart Web code would be placed in:
web/
└── main.dart
Here, web/ is not a Flutter Web platform shell, nor a generated directory to be ignored; it is the source code directory. Under the current situation, the default behavior of fully overwriting and adding all platforms every time would cause the Analyzer to stop scanning web/.
So the solution to this problem was also very simple. The corresponding fix PR #191151 handled it by checking at the beginning of the migration whether the current package actually depends on Flutter:
if (!_project.manifest.dependencies.contains('flutter')) {
return;
}
Pure Dart packages exit directly; the Flutter Tool no longer modifies analysis_options.yaml. Then, the second layer cancels the fixed seven-item list, and the project dynamically generates it based on the platform scaffolds that actually exist in the FlutterProject:
final excludesToExclude = <String>[
'build/**',
if (_project.android.existsSync()) 'android/**',
if (_project.ios.existsSync()) 'ios/**',
if (_project.web.existsSync()) 'web/**',
if (_project.windows.existsSync()) 'windows/**',
if (_project.macos.existsSync()) 'macos/**',
if (_project.linux.existsSync()) 'linux/**',
];
Finally, the flutter create template was also fixed synchronously; otherwise, even if the migration was correct, new project generation would still have issues.
So, essentially, both issues stem from the same problem. Although it's not a major incident, it is purely infuriating. Running a casual Flutter command mysteriously modifies your analysis_options, and as long as it differs from the fixed template, it overwrites without thinking, which is purely annoying.
In fact, this problem was very simple from the start. It's just that the person who fixed it was careless, the person who merged it was also careless, and in the end, it became this mess.