From 7bc66b5e5639be842a9c14808223dcbb061c00ad Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Thu, 7 May 2026 21:25:15 +0530 Subject: [PATCH 1/5] [pigeon] Add support for multiple part files --- packages/pigeon/lib/src/pigeon_lib.dart | 130 +++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/packages/pigeon/lib/src/pigeon_lib.dart b/packages/pigeon/lib/src/pigeon_lib.dart index fd6b6c827b2b..29f83efe9de5 100644 --- a/packages/pigeon/lib/src/pigeon_lib.dart +++ b/packages/pigeon/lib/src/pigeon_lib.dart @@ -13,6 +13,7 @@ import 'package:analyzer/dart/analysis/analysis_context_collection.dart' show AnalysisContextCollection; import 'package:analyzer/dart/analysis/results.dart' show ParsedUnitResult; import 'package:analyzer/dart/analysis/session.dart' show AnalysisSession; +import 'package:analyzer/dart/analysis/utilities.dart' show parseString; import 'package:analyzer/dart/ast/ast.dart' as dart_ast; import 'package:analyzer/diagnostic/diagnostic.dart' show Diagnostic; import 'package:args/args.dart'; @@ -454,14 +455,28 @@ class Pigeon { /// [sdkPath] for specifying the Dart SDK path for /// [AnalysisContextCollection]. ParseResults parseFile(String inputPath, {String? sdkPath}) { - final includedPaths = [path.absolute(path.normalize(inputPath))]; + final String normalizedInputPath = path.absolute(path.normalize(inputPath)); + final String mainContent = _readFileOrThrow(normalizedInputPath); + final List parts = _getPartPaths( + sourcePath: normalizedInputPath, + sourceContent: mainContent, + ); + + final includedPaths = [normalizedInputPath, ...parts]; + final collection = AnalysisContextCollection( includedPaths: includedPaths, sdkPath: sdkPath, ); final compilationErrors = []; - final rootBuilder = RootBuilder(File(inputPath).readAsStringSync()); + final rootBuilder = RootBuilder( + _getInputString( + inputPath: normalizedInputPath, + inputContent: mainContent, + partPaths: parts, + ), + ); for (final AnalysisContext context in collection.contexts) { for (final String path in context.contextRoot.analyzedFiles()) { final AnalysisSession session = context.currentSession; @@ -497,6 +512,117 @@ class Pigeon { } } + String _readFileOrThrow(String filePath) { + final file = File(filePath); + if (!file.existsSync()) { + throw Exception('File ${file.path} does not exist'); + } + return file.readAsStringSync(); + } + + List _getPartPaths({ + required String sourcePath, + required String sourceContent, + }) { + final dart_ast.CompilationUnit unit = parseString( + content: sourceContent, + path: sourcePath, + throwIfDiagnostics: false, + ).unit; + final parts = []; + for (final dart_ast.Directive directive in unit.directives) { + if (directive is dart_ast.PartDirective) { + final String? uri = directive.uri.stringValue; + if (uri != null) { + parts.add( + path.absolute( + path.normalize(path.join(path.dirname(sourcePath), uri)), + ), + ); + } + } + } + return parts; + } + + ({String body, List imports}) _stripPartsAndCollectImports({ + required String sourceContent, + required String sourcePath, + required bool collectImports, + }) { + final dart_ast.CompilationUnit unit = parseString( + content: sourceContent, + path: sourcePath, + throwIfDiagnostics: false, + ).unit; + final imports = []; + final removalRanges = <({int start, int end})>[]; + for (final dart_ast.Directive directive in unit.directives) { + if (directive is dart_ast.ImportDirective) { + if (collectImports) { + imports.add( + sourceContent + .substring(directive.offset, directive.end) + .trimRight(), + ); + } + removalRanges.add((start: directive.offset, end: directive.end)); + } else if (directive is dart_ast.PartDirective || + directive is dart_ast.PartOfDirective) { + removalRanges.add((start: directive.offset, end: directive.end)); + } + } + + final body = StringBuffer(); + var start = 0; + for (final range in removalRanges) { + body.write(sourceContent.substring(start, range.start)); + start = range.end; + } + body.write(sourceContent.substring(start)); + return (body: body.toString().trim(), imports: imports); + } + + String _getInputString({ + required String inputPath, + required String inputContent, + required List partPaths, + }) { + final ({String body, List imports}) mainResult = + _stripPartsAndCollectImports( + sourceContent: inputContent, + sourcePath: inputPath, + collectImports: true, + ); + final List imports = mainResult.imports; + + final partBodies = []; + for (final partPath in partPaths) { + final ({String body, List imports}) partResult = + _stripPartsAndCollectImports( + sourceContent: _readFileOrThrow(partPath), + sourcePath: partPath, + collectImports: false, + ); + partBodies.add(partResult.body); + } + + final output = StringBuffer(); + if (imports.isNotEmpty) { + output.writeln(imports.join('\n')); + output.writeln(); + } + output.writeln(mainResult.body); + for (final partBody in partBodies) { + if (partBody.isNotEmpty) { + output.writeln(); + output.writeln(); + output.write(partBody); + } + } + return output.toString().trimRight(); + } + /// String that describes how the tool is used. static String get usage { return ''' From 9165d7c17b1371ace260a4fb3ae81efc1cc9faf4 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Thu, 7 May 2026 22:03:10 +0530 Subject: [PATCH 2/5] Add test --- packages/pigeon/test/pigeon_lib_test.dart | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 7b2d3b78a1c7..c25d08f69134 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -65,6 +65,24 @@ void main() { return results!; } + ParseResults parseSourceWithParts({ + required String mainSource, + required Map partSources, + }) { + final Pigeon dartle = Pigeon.setup(); + final Directory dir = Directory.systemTemp.createTempSync(); + try { + for (final MapEntry part in partSources.entries) { + File('${dir.path}/${part.key}').writeAsStringSync(part.value); + } + final mainFile = File('${dir.path}/source.dart') + ..writeAsStringSync(mainSource); + return dartle.parseFile(mainFile.path); + } finally { + dir.deleteSync(recursive: true); + } + } + test('parse args - input', () { final PigeonOptions opts = Pigeon.parseArgs([ '--input', @@ -249,6 +267,57 @@ abstract class Api1 { expect(unused?.fields[0].type.isNullable, isTrue); }); + test('parse source split across multiple part files', () { + const mainSource = ''' +part 'shared_classes.dart'; +part 'api.dart'; +'''; + const sharedClassesPart = ''' +part of 'source.dart'; + +class Input1 { + String? input; +} + +class Output1 { + String? output; +} +'''; + const apiPart = ''' +part of 'source.dart'; + +@HostApi() +abstract class Api1 { + Output1 doit(Input1 input); +} +'''; + final ParseResults parseResult = parseSourceWithParts( + mainSource: mainSource, + partSources: { + 'shared_classes.dart': sharedClassesPart, + 'api.dart': apiPart, + }, + ); + + expect(parseResult.errors, isEmpty); + expect(parseResult.root.apis, hasLength(1)); + expect(parseResult.root.apis[0].name, equals('Api1')); + expect(parseResult.root.apis[0].methods, hasLength(1)); + expect(parseResult.root.apis[0].methods[0].name, equals('doit')); + expect(parseResult.root.apis[0].methods[0].returnType.baseName, 'Output1'); + expect(parseResult.root.apis[0].methods[0].parameters, hasLength(1)); + expect( + parseResult.root.apis[0].methods[0].parameters[0].type.baseName, + equals('Input1'), + ); + expect( + parseResult.root.classes.map( + (Class classDefinition) => classDefinition.name, + ), + containsAll(['Input1', 'Output1']), + ); + }); + test('invalid datatype', () { const source = ''' class InvalidDatatype { From f928c6a8254949ddbfe55011f6d5076fb2a5033d Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Thu, 7 May 2026 22:08:08 +0530 Subject: [PATCH 3/5] Resolve Ai comments --- packages/pigeon/lib/src/pigeon_lib.dart | 97 +++++++++++++++-------- packages/pigeon/test/pigeon_lib_test.dart | 10 +++ 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/packages/pigeon/lib/src/pigeon_lib.dart b/packages/pigeon/lib/src/pigeon_lib.dart index 29f83efe9de5..55c68b62012e 100644 --- a/packages/pigeon/lib/src/pigeon_lib.dart +++ b/packages/pigeon/lib/src/pigeon_lib.dart @@ -456,10 +456,27 @@ class Pigeon { /// [AnalysisContextCollection]. ParseResults parseFile(String inputPath, {String? sdkPath}) { final String normalizedInputPath = path.absolute(path.normalize(inputPath)); - final String mainContent = _readFileOrThrow(normalizedInputPath); + final String? mainContent = _readFileOrNull(normalizedInputPath); + if (mainContent == null) { + return ParseResults( + root: Root.makeEmpty(), + errors: [ + Error( + message: 'File $normalizedInputPath does not exist', + filename: normalizedInputPath, + ), + ], + pigeonOptions: null, + ); + } + final dart_ast.CompilationUnit mainUnit = parseString( + content: mainContent, + path: normalizedInputPath, + throwIfDiagnostics: false, + ).unit; final List parts = _getPartPaths( + mainUnit.directives, sourcePath: normalizedInputPath, - sourceContent: mainContent, ); final includedPaths = [normalizedInputPath, ...parts]; @@ -470,21 +487,37 @@ class Pigeon { ); final compilationErrors = []; - final rootBuilder = RootBuilder( - _getInputString( - inputPath: normalizedInputPath, - inputContent: mainContent, - partPaths: parts, - ), + final String? rootInputString = _getInputString( + inputPath: normalizedInputPath, + inputContent: mainContent, + inputUnit: mainUnit, + partPaths: parts, ); + if (rootInputString == null) { + return ParseResults( + root: Root.makeEmpty(), + errors: [ + Error( + message: + 'Part file referenced by $normalizedInputPath does not exist', + filename: normalizedInputPath, + ), + ], + pigeonOptions: null, + ); + } + final rootBuilder = RootBuilder(rootInputString); + final dart_ast.CompilationUnit mergedUnit = parseString( + content: rootInputString, + path: normalizedInputPath, + throwIfDiagnostics: false, + ).unit; + mergedUnit.accept(rootBuilder); for (final AnalysisContext context in collection.contexts) { for (final String path in context.contextRoot.analyzedFiles()) { final AnalysisSession session = context.currentSession; final result = session.getParsedUnit(path) as ParsedUnitResult; - if (result.diagnostics.isEmpty) { - final dart_ast.CompilationUnit unit = result.unit; - unit.accept(rootBuilder); - } else { + if (result.diagnostics.isNotEmpty) { for (final Diagnostic diagnostic in result.diagnostics) { compilationErrors.add( Error( @@ -512,25 +545,20 @@ class Pigeon { } } - String _readFileOrThrow(String filePath) { + String? _readFileOrNull(String filePath) { final file = File(filePath); if (!file.existsSync()) { - throw Exception('File ${file.path} does not exist'); + return null; } return file.readAsStringSync(); } - List _getPartPaths({ + List _getPartPaths( + Iterable directives, { required String sourcePath, - required String sourceContent, }) { - final dart_ast.CompilationUnit unit = parseString( - content: sourceContent, - path: sourcePath, - throwIfDiagnostics: false, - ).unit; final parts = []; - for (final dart_ast.Directive directive in unit.directives) { + for (final directive in directives) { if (directive is dart_ast.PartDirective) { final String? uri = directive.uri.stringValue; if (uri != null) { @@ -547,14 +575,9 @@ class Pigeon { ({String body, List imports}) _stripPartsAndCollectImports({ required String sourceContent, - required String sourcePath, + required dart_ast.CompilationUnit unit, required bool collectImports, }) { - final dart_ast.CompilationUnit unit = parseString( - content: sourceContent, - path: sourcePath, - throwIfDiagnostics: false, - ).unit; final imports = []; final removalRanges = <({int start, int end})>[]; for (final dart_ast.Directive directive in unit.directives) { @@ -583,25 +606,35 @@ class Pigeon { return (body: body.toString().trim(), imports: imports); } - String _getInputString({ + String? _getInputString({ required String inputPath, required String inputContent, + required dart_ast.CompilationUnit inputUnit, required List partPaths, }) { final ({String body, List imports}) mainResult = _stripPartsAndCollectImports( sourceContent: inputContent, - sourcePath: inputPath, + unit: inputUnit, collectImports: true, ); final List imports = mainResult.imports; final partBodies = []; for (final partPath in partPaths) { + final String? partContent = _readFileOrNull(partPath); + if (partContent == null) { + return null; + } + final dart_ast.CompilationUnit partUnit = parseString( + content: partContent, + path: partPath, + throwIfDiagnostics: false, + ).unit; final ({String body, List imports}) partResult = _stripPartsAndCollectImports( - sourceContent: _readFileOrThrow(partPath), - sourcePath: partPath, + sourceContent: partContent, + unit: partUnit, collectImports: false, ); partBodies.add(partResult.body); diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index c25d08f69134..559fd78568fd 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -318,6 +318,16 @@ abstract class Api1 { ); }); + test('missing part file returns parse error', () { + const source = ''' +part 'missing.dart'; +'''; + final ParseResults parseResult = parseSource(source); + expect(parseResult.root.apis, isEmpty); + expect(parseResult.root.classes, isEmpty); + expect(parseResult.errors, isNotEmpty); + }); + test('invalid datatype', () { const source = ''' class InvalidDatatype { From 3c100e575d635c9fb4dc01dd50b638cf85512925 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 8 May 2026 21:40:24 +0530 Subject: [PATCH 4/5] Resolve Ai comments --- packages/pigeon/lib/src/pigeon_lib.dart | 132 +++++++++++++--------- packages/pigeon/test/pigeon_lib_test.dart | 38 ++++++- 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/packages/pigeon/lib/src/pigeon_lib.dart b/packages/pigeon/lib/src/pigeon_lib.dart index 55c68b62012e..6e34e95c1c0d 100644 --- a/packages/pigeon/lib/src/pigeon_lib.dart +++ b/packages/pigeon/lib/src/pigeon_lib.dart @@ -456,56 +456,32 @@ class Pigeon { /// [AnalysisContextCollection]. ParseResults parseFile(String inputPath, {String? sdkPath}) { final String normalizedInputPath = path.absolute(path.normalize(inputPath)); - final String? mainContent = _readFileOrNull(normalizedInputPath); - if (mainContent == null) { + final _CollectedInput input = _collectInputAndParts(normalizedInputPath); + if (input.missingPath != null) { return ParseResults( root: Root.makeEmpty(), errors: [ Error( - message: 'File $normalizedInputPath does not exist', - filename: normalizedInputPath, + message: 'File ${input.missingPath} does not exist', + filename: input.missingPath, ), ], pigeonOptions: null, ); } - final dart_ast.CompilationUnit mainUnit = parseString( - content: mainContent, - path: normalizedInputPath, - throwIfDiagnostics: false, - ).unit; - final List parts = _getPartPaths( - mainUnit.directives, - sourcePath: normalizedInputPath, - ); - - final includedPaths = [normalizedInputPath, ...parts]; final collection = AnalysisContextCollection( - includedPaths: includedPaths, + includedPaths: input.paths, sdkPath: sdkPath, ); final compilationErrors = []; - final String? rootInputString = _getInputString( + final String rootInputString = _getInputString( inputPath: normalizedInputPath, - inputContent: mainContent, - inputUnit: mainUnit, - partPaths: parts, + contents: input.contents, + units: input.units, + paths: input.paths, ); - if (rootInputString == null) { - return ParseResults( - root: Root.makeEmpty(), - errors: [ - Error( - message: - 'Part file referenced by $normalizedInputPath does not exist', - filename: normalizedInputPath, - ), - ], - pigeonOptions: null, - ); - } final rootBuilder = RootBuilder(rootInputString); final dart_ast.CompilationUnit mergedUnit = parseString( content: rootInputString, @@ -514,7 +490,7 @@ class Pigeon { ).unit; mergedUnit.accept(rootBuilder); for (final AnalysisContext context in collection.contexts) { - for (final String path in context.contextRoot.analyzedFiles()) { + for (final String path in input.paths) { final AnalysisSession session = context.currentSession; final result = session.getParsedUnit(path) as ParsedUnitResult; if (result.diagnostics.isNotEmpty) { @@ -591,7 +567,8 @@ class Pigeon { } removalRanges.add((start: directive.offset, end: directive.end)); } else if (directive is dart_ast.PartDirective || - directive is dart_ast.PartOfDirective) { + directive is dart_ast.PartOfDirective || + directive is dart_ast.LibraryDirective) { removalRanges.add((start: directive.offset, end: directive.end)); } } @@ -606,35 +583,25 @@ class Pigeon { return (body: body.toString().trim(), imports: imports); } - String? _getInputString({ + String _getInputString({ required String inputPath, - required String inputContent, - required dart_ast.CompilationUnit inputUnit, - required List partPaths, + required List paths, + required Map contents, + required Map units, }) { final ({String body, List imports}) mainResult = _stripPartsAndCollectImports( - sourceContent: inputContent, - unit: inputUnit, + sourceContent: contents[inputPath]!, + unit: units[inputPath]!, collectImports: true, ); final List imports = mainResult.imports; - final partBodies = []; - for (final partPath in partPaths) { - final String? partContent = _readFileOrNull(partPath); - if (partContent == null) { - return null; - } - final dart_ast.CompilationUnit partUnit = parseString( - content: partContent, - path: partPath, - throwIfDiagnostics: false, - ).unit; + for (final String currentPath in paths.skip(1)) { final ({String body, List imports}) partResult = _stripPartsAndCollectImports( - sourceContent: partContent, - unit: partUnit, + sourceContent: contents[currentPath]!, + unit: units[currentPath]!, collectImports: false, ); partBodies.add(partResult.body); @@ -656,6 +623,49 @@ class Pigeon { return output.toString().trimRight(); } + _CollectedInput _collectInputAndParts(String inputPath) { + final paths = []; + final contents = {}; + final units = {}; + final pending = [inputPath]; + final seen = {}; + while (pending.isNotEmpty) { + final String currentPath = pending.removeAt(0); + if (seen.contains(currentPath)) { + continue; + } + seen.add(currentPath); + final String? content = _readFileOrNull(currentPath); + if (content == null) { + return _CollectedInput( + paths: paths, + contents: contents, + units: units, + missingPath: currentPath, + ); + } + final dart_ast.CompilationUnit unit = parseString( + content: content, + path: currentPath, + throwIfDiagnostics: false, + ).unit; + paths.add(currentPath); + contents[currentPath] = content; + units[currentPath] = unit; + final List partPaths = _getPartPaths( + unit.directives, + sourcePath: currentPath, + ); + pending.addAll(partPaths); + } + return _CollectedInput( + paths: paths, + contents: contents, + units: units, + missingPath: null, + ); + } + /// String that describes how the tool is used. static String get usage { return ''' @@ -1021,3 +1031,17 @@ class ParseResults { /// [ConfigurePigeon] during parsing. final Map? pigeonOptions; } + +class _CollectedInput { + _CollectedInput({ + required this.paths, + required this.contents, + required this.units, + required this.missingPath, + }); + + final List paths; + final Map contents; + final Map units; + final String? missingPath; +} diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 559fd78568fd..6adf7c333000 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -73,7 +73,9 @@ void main() { final Directory dir = Directory.systemTemp.createTempSync(); try { for (final MapEntry part in partSources.entries) { - File('${dir.path}/${part.key}').writeAsStringSync(part.value); + final partFile = File('${dir.path}/${part.key}'); + partFile.createSync(recursive: true); + partFile.writeAsStringSync(part.value); } final mainFile = File('${dir.path}/source.dart') ..writeAsStringSync(mainSource); @@ -328,6 +330,40 @@ part 'missing.dart'; expect(parseResult.errors, isNotEmpty); }); + test('part errors keep part file line numbers', () { + const mainSource = ''' +part 'api.dart'; +part 'extra.dart'; +'''; + const apiPart = ''' +part of 'source.dart'; + +@HostApi() +@FlutterApi() +abstract class Api1 { + void ping(); +} +'''; + const extraPart = ''' +part of 'source.dart'; +class Extra { + int? value; +} +'''; + final ParseResults parseResult = parseSourceWithParts( + mainSource: mainSource, + partSources: { + 'api.dart': apiPart, + 'extra.dart': extraPart, + }, + ); + final Error apiAnnotationError = parseResult.errors.firstWhere( + (Error error) => + error.message.contains('can only have one API annotation'), + ); + expect(apiAnnotationError.lineNumber, 4); + }); + test('invalid datatype', () { const source = ''' class InvalidDatatype { From dd3b0fbc84d312668d9f9fc2895dcd3fba39817e Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 8 May 2026 22:35:44 +0530 Subject: [PATCH 5/5] Update changelog --- packages/pigeon/CHANGELOG.md | 5 +++++ packages/pigeon/lib/src/generator_tools.dart | 2 +- packages/pigeon/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 1d8884d3e71c..d3a14ccbaab4 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,8 @@ +## 26.3.5 + +* Adds support for parsing Pigeon definitions split across multiple Dart `part` + files. + ## 26.3.4 * [kotlin] Updates generated error class to inherit from `RuntimeException` diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index 55874a95684a..c4870feba80f 100644 --- a/packages/pigeon/lib/src/generator_tools.dart +++ b/packages/pigeon/lib/src/generator_tools.dart @@ -15,7 +15,7 @@ import 'generator.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '26.3.4'; +const String pigeonVersion = '26.3.5'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 804181258e69..cc14e1b3ee5a 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,7 +2,7 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22 -version: 26.3.4 # This must match the version in lib/src/generator_tools.dart +version: 26.3.5 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.9.0