[cross imports] Check examples cross imports#187662
Conversation
491ff00 to
81a16df
Compare
There was a problem hiding this comment.
Code Review
This pull request extracts shared cross-import checking logic into a new utility file, cross_imports_checker_utils.dart, and introduces a new script, check_examples_cross_imports.dart, to check examples for cross imports, along with corresponding tests and test utilities. The review feedback points out multiple instances where Platform.pathSeparator or the global path package are used directly, which can cause test failures on Windows or when using a MemoryFileSystem with a different path style. The reviewer recommends using the file-system-specific path context (e.g., file.fileSystem.path) to ensure robust cross-platform compatibility.
| factory _ExamplesLibrary.fromDirectory(Directory directory, {required Directory flutterRoot}) { | ||
| if (!directory.absolute.path.startsWith(flutterRoot.absolute.path)) { | ||
| throw ArgumentError('Directory must be inside ${flutterRoot.absolute.path}.', 'directory'); | ||
| } | ||
|
|
||
| final String relativePath = path | ||
| .relative(directory.absolute.path, from: flutterRoot.absolute.path) | ||
| .replaceAll(Platform.pathSeparator, '/'); |
There was a problem hiding this comment.
Using path.relative and Platform.pathSeparator directly can cause test failures on Windows CI or when running tests with a different file system style (e.g., using a Windows MemoryFileSystem on a POSIX host). Additionally, path casing on Windows (e.g., C:\ vs c:\) can cause startsWith to fail. It is highly recommended to use directory.fileSystem.path and canonicalize the paths first to ensure robust cross-platform compatibility.
factory _ExamplesLibrary.fromDirectory(Directory directory, {required Directory flutterRoot}) {
final String canonicalDirectoryPath = directory.fileSystem.path.canonicalize(directory.absolute.path);
final String canonicalFlutterRootPath = directory.fileSystem.path.canonicalize(flutterRoot.absolute.path);
if (!canonicalDirectoryPath.startsWith(canonicalFlutterRootPath)) {
throw ArgumentError('Directory must be inside ${flutterRoot.absolute.path}.', 'directory');
}
final String relativePath = directory.fileSystem.path
.relative(directory.absolute.path, from: flutterRoot.absolute.path)
.replaceAll(directory.fileSystem.path.separator, '/');| Set<String> differencePaths(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { | ||
| final Set<String> testPaths = files.map((File file) { | ||
| final int index = file.absolute.path.indexOf(prefix); | ||
| if (index < 0) { | ||
| throw ArgumentError('All files must include $prefix in their path.', 'files'); | ||
| } | ||
|
|
||
| return file.absolute.path.substring(index).replaceAll(Platform.pathSeparator, '/'); | ||
| }).toSet(); |
There was a problem hiding this comment.
Using Platform.pathSeparator can cause issues when running tests using a MemoryFileSystem with a different path style than the host platform. Use file.fileSystem.path.separator instead to ensure the path separators are correctly replaced regardless of the host platform.
| Set<String> differencePaths(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { | |
| final Set<String> testPaths = files.map((File file) { | |
| final int index = file.absolute.path.indexOf(prefix); | |
| if (index < 0) { | |
| throw ArgumentError('All files must include $prefix in their path.', 'files'); | |
| } | |
| return file.absolute.path.substring(index).replaceAll(Platform.pathSeparator, '/'); | |
| }).toSet(); | |
| Set<String> differencePaths(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { | |
| final Set<String> testPaths = files.map((File file) { | |
| final int index = file.absolute.path.indexOf(prefix); | |
| if (index < 0) { | |
| throw ArgumentError('All files must include $prefix in their path.', 'files'); | |
| } | |
| return file.absolute.path.substring(index).replaceAll(file.fileSystem.path.separator, '/'); | |
| }).toSet(); |
| String getImportError({ | ||
| required Set<File> files, | ||
| required Directory flutterRoot, | ||
| required CrossImportCheckedLibrary checkedLibrary, | ||
| required LibraryCrossImportStatementType importStatement, | ||
| }) { | ||
| assert(!checkedLibrary.canImport(importStatement), checkedLibrary.cannotImportMessage); | ||
|
|
||
| final String importedLibraryName = importStatement.readableName; | ||
| final buffer = StringBuffer( | ||
| checkedLibrary.getDisallowedImportMessage(importedLibraryName, files.length), | ||
| ); | ||
| for (final file in files) { | ||
| buffer.writeln( | ||
| ' ${getRelativePath(file, flutterRoot: flutterRoot).replaceAll(Platform.pathSeparator, '/')}', | ||
| ); | ||
| } | ||
| return buffer.toString().trimRight(); | ||
| } | ||
|
|
||
| /// Returns the [file]'s relative path, relative to [flutterRoot]. | ||
| String getRelativePath(File file, {required Directory flutterRoot}) { | ||
| return path.relative(file.absolute.path, from: flutterRoot.absolute.path); |
There was a problem hiding this comment.
Using the global path context and Platform.pathSeparator directly can cause test failures on Windows or when running tests with a different file system style. Use file.fileSystem.path and flutterRoot.fileSystem.path to ensure correct path operations.
String getImportError({
required Set<File> files,
required Directory flutterRoot,
required CrossImportCheckedLibrary checkedLibrary,
required LibraryCrossImportStatementType importStatement,
}) {
assert(!checkedLibrary.canImport(importStatement), checkedLibrary.cannotImportMessage);
final String importedLibraryName = importStatement.readableName;
final buffer = StringBuffer(
checkedLibrary.getDisallowedImportMessage(importedLibraryName, files.length),
);
for (final file in files) {
buffer.writeln(
' ${getRelativePath(file, flutterRoot: flutterRoot).replaceAll(file.fileSystem.path.separator, '/')}',
);
}
return buffer.toString().trimRight();
}
/// Returns the [file]'s relative path, relative to [flutterRoot].
String getRelativePath(File file, {required Directory flutterRoot}) {
return flutterRoot.fileSystem.path.relative(file.absolute.path, from: flutterRoot.absolute.path);
}| Set<File> getUnknowns(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { | ||
| return files.where((File file) { | ||
| final int index = file.absolute.path.indexOf(prefix); | ||
|
|
||
| if (index < 0) { | ||
| throw ArgumentError('All files must include $prefix in their path.', 'files'); | ||
| } | ||
|
|
||
| final String comparablePath = file.absolute.path | ||
| .substring(index) | ||
| .replaceAll(Platform.pathSeparator, '/'); | ||
|
|
||
| return !knownPaths.contains(comparablePath); | ||
| }).toSet(); |
There was a problem hiding this comment.
Using Platform.pathSeparator can cause issues when running tests using a MemoryFileSystem with a different path style than the host platform. Use file.fileSystem.path.separator instead to ensure the path separators are correctly replaced regardless of the host platform.
| Set<File> getUnknowns(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { | |
| return files.where((File file) { | |
| final int index = file.absolute.path.indexOf(prefix); | |
| if (index < 0) { | |
| throw ArgumentError('All files must include $prefix in their path.', 'files'); | |
| } | |
| final String comparablePath = file.absolute.path | |
| .substring(index) | |
| .replaceAll(Platform.pathSeparator, '/'); | |
| return !knownPaths.contains(comparablePath); | |
| }).toSet(); | |
| Set<File> getUnknowns(Set<String> knownPaths, Set<File> files, {required Pattern prefix}) { | |
| return files.where((File file) { | |
| final int index = file.absolute.path.indexOf(prefix); | |
| if (index < 0) { | |
| throw ArgumentError('All files must include $prefix in their path.', 'files'); | |
| } | |
| final String comparablePath = file.absolute.path | |
| .substring(index) | |
| .replaceAll(file.fileSystem.path.separator, '/'); | |
| return !knownPaths.contains(comparablePath); | |
| }).toSet(); |
| File getFile(String filepath, Directory directory) { | ||
| final String platformFilepath = filepath.replaceAll('/', Platform.pathSeparator); | ||
| final String searchPattern = directory.basename + Platform.pathSeparator; | ||
| // Don't use `lastIndexOf`, as for files in test fixes | ||
| // i.e. `packages/flutter_test/test_fixes/flutter_test/matchers.dart` | ||
| // the overlap index could appear multiple times. | ||
| // Only take the first one. | ||
| final int overlapIndex = platformFilepath.indexOf(searchPattern); | ||
|
|
||
| if (overlapIndex < 0) { | ||
| throw ArgumentError('filepath $filepath must be located in directory ${directory.path}.'); | ||
| } | ||
|
|
||
| final String filename = platformFilepath.substring(overlapIndex + searchPattern.length); | ||
| return directory.childFile(filename); |
There was a problem hiding this comment.
Using Platform.pathSeparator and directory.basename directly can cause test failures or compilation errors. It is safer and more robust to use directory.fileSystem.path to retrieve the path separator and basename context-awarely.
| File getFile(String filepath, Directory directory) { | |
| final String platformFilepath = filepath.replaceAll('/', Platform.pathSeparator); | |
| final String searchPattern = directory.basename + Platform.pathSeparator; | |
| // Don't use `lastIndexOf`, as for files in test fixes | |
| // i.e. `packages/flutter_test/test_fixes/flutter_test/matchers.dart` | |
| // the overlap index could appear multiple times. | |
| // Only take the first one. | |
| final int overlapIndex = platformFilepath.indexOf(searchPattern); | |
| if (overlapIndex < 0) { | |
| throw ArgumentError('filepath $filepath must be located in directory ${directory.path}.'); | |
| } | |
| final String filename = platformFilepath.substring(overlapIndex + searchPattern.length); | |
| return directory.childFile(filename); | |
| File getFile(String filepath, Directory directory) { | |
| final String platformFilepath = filepath.replaceAll('/', directory.fileSystem.path.separator); | |
| final String searchPattern = directory.fileSystem.path.basename(directory.path) + directory.fileSystem.path.separator; | |
| // Don't use `lastIndexOf`, as for files in test fixes | |
| // i.e. `packages/flutter_test/test_fixes/flutter_test/matchers.dart` | |
| // the overlap index could appear multiple times. | |
| // Only take the first one. | |
| final int overlapIndex = platformFilepath.indexOf(searchPattern); | |
| if (overlapIndex < 0) { | |
| throw ArgumentError('filepath $filepath must be located in directory ${directory.path}.'); | |
| } | |
| final String filename = platformFilepath.substring(overlapIndex + searchPattern.length); | |
| return directory.childFile(filename); | |
| } |
| /// image, creates single flutter widget with five copies of requested | ||
| /// image and prints how long the loading took. | ||
| /// | ||
| /// This is used in [$FH/flutter/devicelab/bin/tasks/image_list_reported_duration.dart] test. |
There was a problem hiding this comment.
Noticed this while fixing up a test. I don't know what $FH is, so I removed it.
| @@ -0,0 +1,1440 @@ | |||
| // Copyright 2014 The Flutter Authors. All rights reserved. | |||
There was a problem hiding this comment.
There's a bunch of big switch cases and the new known import lists are also pretty big.
8e010db to
bc37d36
Compare
bc37d36 to
d6b87d5
Compare
d6b87d5 to
56c21d0
Compare
56c21d0 to
2aa3eb5
Compare
497e404 to
749570f
Compare
| /// that we can catch any new cross imports that are added. | ||
| // TODO(justinmc): Fix all of these tests so there are no cross imports. | ||
| // See https://github.com/flutter/flutter/issues/187645. | ||
| static final Set<String> knownExamplesSlashApiCupertinoCrossImports = <String>{ |
There was a problem hiding this comment.
So, I don't know that we need to address cross imports in cupertino or material. All of the sample code has moved over, so there should be no need to make additional changes to those directories. But further, cross imports is I think a lot less strict wrt examples/api. For example, I moved over an example for the cupertino_ui docs in https://github.com/flutter/packages/pull/12086/files that contained no cupertino code, but it was a helpful, contextual example that was already embedded in the docs.
Eventually we will delete the material/ and cupertino/ API examples directories, so I think being able to do that should be the main focus this helps enforce leading up to that, WDYT?
There was a problem hiding this comment.
I agree, eventually material/cupertino examples go in material_ui/cupertino_ui
This PR is mainly to flag examples in the core widgets/painting/rendering etc layers, which should ever only import widgets.
There was a problem hiding this comment.
Opened flutter/packages#12228 to fix the upstream examples.
Eventually the checker can be updated when the old examples are removed, but this unblocks landing the checker during the code freeze period :)
3bc100c to
52eb703
Compare
|
@Piinks Updated the examples cross imports checker with the following:
So yes, now there are no more Cupertino samples with cross imports! However, this looks to break the code freeze check. I'll post said commit to cupertino_ui instead, but we will have to delete the example here when the time comes. |
5b36af3 to
6ee4f53
Compare
Piinks
left a comment
There was a problem hiding this comment.
Hey @navaronbracke, thanks for working on this! The shared utility refactoring in cross_imports_checker_utils.dart and cross-platform path handling look great.
Before we land this, I think we should simplify the scope of check_examples_cross_imports.dart to better align with the sample migration strategy:
- The primary goal of cross-import checking is to unblock the relocation and eventual deletion of
materialandcupertinocode and samples fromflutter/flutterinto the standalonematerial_uiandcupertino_uipackage repos. - Since the Material and Cupertino API samples (
examples/api/lib/material,examples/api/lib/cupertino, etc.) have already been relocated tomaterial_uiandcupertino_ui, their legacy copies here are temporary placeholders waiting for deletion, so we don't need to check or maintain baseline lists for them (knownExamplesSlashApiMaterialCrossImports/knownExamplesSlashApiCupertinoCrossImports). - I don't think we need to enforce strict subfolder boundaries within core framework samples (e.g. banning
renderingimports insidewidgetssamples). Core framework layers are naturally interdependent, and enforcing granular subfolder separation creates unnecessary churn and large baseline maintenance overhead.
So here is what I suggest, curious too for what @justinmc thinks:
- Treat
examples/api(or generic non-Material/non-Cupertino samples) as a single library check verifying they don't introduce Material/Cupertino dependencies. - Omit
material/andcupertino/subfolders underexamples/apifrom the check. - Collapse the 12+ separate
knownExamplesSlashApi...CrossImportsbaseline sets into a much simpler, smaller set.
LMKWYT and thanks again!
|
The CI failure here is just a timeout, all good. 👍 |
Yeah, that sounds reasonable wrt the current situation and where we're going to. |
This PR adds a check that checks examples under /examples for cross imports.
Since most of the structure is shared with the existing cross imports checker, I opted to share some bits and pieces in a utils file as well.
A new test suite was added, which mirrors most of the setup in the existing test.
Currently the examples checker is too strict by design, as it does not allow any cross imports at all (including Material examples importing Cupertino). There are some edge cases that we should fix (such as context menu examples?) where both imports are allowed. But for now, since we have no checks in place, I opted for strict first,
exempt later, since this gives us a good starting point.
I did notice that examples like
examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart,could definitely be cleaned up before we add exemptions. To be looked at later.
I also copied over a small fix into the cross imports checker that I needed in #187587
relating to
lastIndexOf(), just to make my job a bit easier when either of those PR's lands first.Lastly, I found 4 violations of cross imports for some Cupertino examples, which have been fixed as well, as part of this effort.
Fixes #187645
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.