Skip to content

[cross imports] Check examples cross imports#187662

Open
navaronbracke wants to merge 38 commits into
flutter:masterfrom
navaronbracke:check_examples_cross_imports
Open

[cross imports] Check examples cross imports#187662
navaronbracke wants to merge 38 commits into
flutter:masterfrom
navaronbracke:check_examples_cross_imports

Conversation

@navaronbracke

@navaronbracke navaronbracke commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

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-assist bot 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.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jun 7, 2026
@navaronbracke
navaronbracke requested a review from justinmc June 7, 2026 20:09
@Piinks Piinks added the framework flutter/packages/flutter repository. See also f: labels. label Jun 8, 2026
@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from 491ff00 to 81a16df Compare June 13, 2026 09:32
@github-actions github-actions Bot removed framework flutter/packages/flutter repository. See also f: labels. CICD Run CI/CD labels Jun 13, 2026
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jun 13, 2026
@github-actions github-actions Bot added the d: examples Sample code and demos label Jun 13, 2026
@navaronbracke
navaronbracke marked this pull request as ready for review June 13, 2026 16:37

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1177 to +1184
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, '/');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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, '/');

Comment on lines +81 to +89
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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();

Comment on lines +153 to +175
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Comment on lines +181 to +194
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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();

Comment on lines +51 to +65
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bunch of big switch cases and the new known import lists are also pretty big.

@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from 8e010db to bc37d36 Compare June 14, 2026 08:57
@github-actions github-actions Bot removed the CICD Run CI/CD label Jun 14, 2026
@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from bc37d36 to d6b87d5 Compare June 30, 2026 09:03
@navaronbracke
navaronbracke requested a review from Piinks July 2, 2026 10:28
@navaronbracke navaronbracke added the CICD Run CI/CD label Jul 3, 2026
@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from d6b87d5 to 56c21d0 Compare July 4, 2026 07:52
@Piinks Piinks added the framework flutter/packages/flutter repository. See also f: labels. label Jul 6, 2026
@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from 56c21d0 to 2aa3eb5 Compare July 7, 2026 06:35
@github-actions github-actions Bot removed the framework flutter/packages/flutter repository. See also f: labels. label Jul 7, 2026
@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch 2 times, most recently from 497e404 to 749570f Compare July 8, 2026 13:20
/// 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>{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from 3bc100c to 52eb703 Compare July 16, 2026 06:16
@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository d: api docs Issues with https://api.flutter.dev/ labels Jul 16, 2026
@navaronbracke

navaronbracke commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@Piinks Updated the examples cross imports checker with the following:

  • since the Cupertino magnifier examples were moved from examples/api/widgets to examples/api/cupertino there is no more cross import for those, so they have been removed from the known cross imports
  • the remaining cross imports for the Cupertino examples have been fixed:
  1. Two tests had a convenience import on Material, instead of Cupertino
  2. For the list tile example, it imported Material for the Icons class + Colors.lightBlue. I decided to let the examples depend on the version of cupertino_icons that the root workspace provides, and used a CupertinoIcons equivalent + a plain blue color as replacement

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.

@navaronbracke
navaronbracke force-pushed the check_examples_cross_imports branch from 5b36af3 to 6ee4f53 Compare July 17, 2026 07:27
@github-actions github-actions Bot removed framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository d: api docs Issues with https://api.flutter.dev/ labels Jul 17, 2026
@Piinks Piinks added the team-framework Owned by Framework team label Jul 20, 2026

@Piinks Piinks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The primary goal of cross-import checking is to unblock the relocation and eventual deletion of material and cupertino code and samples from flutter/flutter into the standalone material_ui and cupertino_ui package repos.
  2. Since the Material and Cupertino API samples (examples/api/lib/material, examples/api/lib/cupertino, etc.) have already been relocated to material_ui and cupertino_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).
  3. I don't think we need to enforce strict subfolder boundaries within core framework samples (e.g. banning rendering imports inside widgets samples). 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/ and cupertino/ subfolders under examples/api from the check.
  • Collapse the 12+ separate knownExamplesSlashApi...CrossImports baseline sets into a much simpler, smaller set.

LMKWYT and thanks again!

@Piinks

Piinks commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The CI failure here is just a timeout, all good. 👍

@navaronbracke

Copy link
Copy Markdown
Contributor Author

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:

  1. The primary goal of cross-import checking is to unblock the relocation and eventual deletion of material and cupertino code and samples from flutter/flutter into the standalone material_ui and cupertino_ui package repos.
  2. Since the Material and Cupertino API samples (examples/api/lib/material, examples/api/lib/cupertino, etc.) have already been relocated to material_ui and cupertino_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).
  3. I don't think we need to enforce strict subfolder boundaries within core framework samples (e.g. banning rendering imports inside widgets samples). 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/ and cupertino/ subfolders under examples/api from the check.
  • Collapse the 12+ separate knownExamplesSlashApi...CrossImports baseline sets into a much simpler, smaller set.

LMKWYT and thanks again!

Yeah, that sounds reasonable wrt the current situation and where we're going to.
I'll implement those adjustments and let you know when this can be reviewed again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD d: examples Sample code and demos team-framework Owned by Framework team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cross imports] Check cross imports in /examples

2 participants