Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 27.2.0

* Adds support for parsing Pigeon definitions split across multiple Dart `part`
files.

## 27.1.0

* [swift] Adds `CaseIterable` conformance to generated enums.
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/src/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'generator.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '27.1.0';
const String pigeonVersion = '27.2.0';

/// Default plugin package name.
const String defaultPluginPackageName = 'dev.flutter.pigeon';
Expand Down
207 changes: 198 additions & 9 deletions packages/pigeon/lib/src/pigeon_lib.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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';
Expand Down Expand Up @@ -450,25 +451,54 @@ class Pigeon {
/// [sdkPath] for specifying the Dart SDK path for
/// [AnalysisContextCollection].
ParseResults parseFile(String inputPath, {String? sdkPath}) {
final includedPaths = <String>[path.absolute(path.normalize(inputPath))];
final collection = AnalysisContextCollection(includedPaths: includedPaths, sdkPath: sdkPath);
final String normalizedInputPath = path.absolute(path.normalize(inputPath));
final _CollectedInput input = _collectInputAndParts(normalizedInputPath);
if (input.missingPath != null) {
return ParseResults(
root: Root.makeEmpty(),
errors: <Error>[
Error(
message: 'File ${input.missingPath} does not exist',
filename: input.missingPath,
),
],
pigeonOptions: null,
);
}

final collection = AnalysisContextCollection(
includedPaths: input.paths,
sdkPath: sdkPath,
);

final compilationErrors = <Error>[];
final rootBuilder = RootBuilder(File(inputPath).readAsStringSync());
final String rootInputString = _getInputString(
inputPath: normalizedInputPath,
contents: input.contents,
units: input.units,
paths: input.paths,
);
final rootBuilder = RootBuilder(rootInputString);
final dart_ast.CompilationUnit mergedUnit = parseString(
content: rootInputString,
path: normalizedInputPath,
throwIfDiagnostics: false,
).unit;
mergedUnit.accept(rootBuilder);
Comment on lines +481 to +487

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.

high

Merging the contents of the main file and its parts into a single rootInputString causes a regression in error reporting. Any errors identified by RootBuilder (e.g., invalid types, unsupported constructs) will report line numbers relative to this concatenated string and attribute them to the main file path, making it difficult for users to locate the actual issue in their source files. Consider a strategy that preserves the mapping to original files or avoids string concatenation by visiting multiple CompilationUnits while providing the correct source context for each.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

mergedUnit.accept(rootBuilder) runs unconditionally before the diagnostics loop below, whereas the previous code only invoked the visitor when a file had no diagnostics. If any input file has syntax errors, the merged source can be partially malformed and the rootBuilder may walk a broken AST and emit spurious Pigeon errors alongside the real syntax errors. Consider deferring this call until after the loop and guarding it on compilationErrors.isEmpty to match the old behavior.

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.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(
message: diagnostic.message,
filename: diagnostic.source.fullName,
lineNumber: calculateLineNumber(diagnostic.source.contents.data, diagnostic.offset),
lineNumber: calculateLineNumber(
diagnostic.source.contents.data,
diagnostic.offset,
),
),
);
}
Expand All @@ -483,6 +513,151 @@ class Pigeon {
}
}

String? _readFileOrNull(String filePath) {
final file = File(filePath);
if (!file.existsSync()) {
return null;
}
return file.readAsStringSync();
}

List<String> _getPartPaths(
Iterable<dart_ast.Directive> directives, {
required String sourcePath,
}) {
final parts = <String>[];
for (final directive in 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;
}
Comment on lines +524 to +542

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

The current implementation of _getPartPaths only retrieves part files referenced directly in the main Pigeon file. It does not support nested part files (i.e., a part file that itself contains part directives), which is valid Dart. To fully support the part mechanism, this should recursively resolve part paths.


({String body, List<String> imports}) _stripPartsAndCollectImports({
required String sourceContent,
required dart_ast.CompilationUnit unit,
required bool collectImports,
}) {
final imports = <String>[];
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 ||
directive is dart_ast.LibraryDirective) {
removalRanges.add((start: directive.offset, end: directive.end));
}
Comment on lines +561 to +565

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

The _stripPartsAndCollectImports function does not handle LibraryDirectives. If the main Pigeon file contains a library directive, it will remain in the body while imports are moved to the top in _getInputString. This results in invalid Dart code (as library must precede import), which may cause the subsequent parseString call or RootBuilder to behave unexpectedly. Consider stripping LibraryDirective as well.

      } else if (directive is dart_ast.PartDirective ||
          directive is dart_ast.PartOfDirective ||
          directive is dart_ast.LibraryDirective) {
        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 List<String> paths,
required Map<String, String> contents,
required Map<String, dart_ast.CompilationUnit> units,
}) {
final ({String body, List<String> imports}) mainResult =
_stripPartsAndCollectImports(
sourceContent: contents[inputPath]!,
unit: units[inputPath]!,
collectImports: true,
);
final List<String> imports = mainResult.imports;
final partBodies = <String>[];
for (final String currentPath in paths.skip(1)) {
final ({String body, List<String> imports}) partResult =
_stripPartsAndCollectImports(
sourceContent: contents[currentPath]!,
unit: units[currentPath]!,
collectImports: false,
);
partBodies.add(partResult.body);
}

final output = StringBuffer();
if (imports.isNotEmpty) {
output.writeln(imports.join('\n'));
output.writeln();
}
output.writeln(mainResult.body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When mainResult.body is empty (e.g. a file containing only part directives, like the test case), writeln('') still emits a newline and the loop below then adds two more newlines before the first part body, so the merged source can begin with three blank lines before any code. This shifts all subsequent line numbers in error reports relative to the original files. Consider only emitting separators between non-empty chunks so the merged source has predictable offsets.

for (final partBody in partBodies) {
if (partBody.isNotEmpty) {
output.writeln();
output.writeln();
output.write(partBody);
}
}
return output.toString().trimRight();
}

_CollectedInput _collectInputAndParts(String inputPath) {
final paths = <String>[];
final contents = <String, String>{};
final units = <String, dart_ast.CompilationUnit>{};
final pending = <String>[inputPath];
final seen = <String>{};
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<String> 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 '''
Expand Down Expand Up @@ -796,3 +971,17 @@ class ParseResults {
/// [ConfigurePigeon] during parsing.
final Map<String, Object>? pigeonOptions;
}

class _CollectedInput {
_CollectedInput({
required this.paths,
required this.contents,
required this.units,
required this.missingPath,
});

final List<String> paths;
final Map<String, String> contents;
final Map<String, dart_ast.CompilationUnit> units;
final String? missingPath;
}
54 changes: 2 additions & 52 deletions packages/pigeon/pigeons/core_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import 'package:pigeon/pigeon.dart';

part 'core_tests_small_apis.dart';

enum AnEnum { one, two, three, fortyTwo, fourHundredTwentyTwo }

// Enums require special logic, having multiple ensures that the logic can be
Expand Down Expand Up @@ -1413,55 +1415,3 @@ abstract class FlutterIntegrationCoreApi {
@SwiftFunction('echoAsync(_:)')
String echoAsyncString(String aString);
}

/// An API that can be implemented for minimal, compile-only tests.
//
// This is also here to test that multiple host APIs can be generated
// successfully in all languages (e.g., in Java where it requires having a
// wrapper class).
@HostApi()
abstract class HostTrivialApi {
void noop();
}

/// A simple API implemented in some unit tests.
//
// This is separate from HostIntegrationCoreApi to avoid having to update a
// lot of unit tests every time we add something to the integration test API.
// TODO(stuartmorgan): Restructure the unit tests to reduce the number of
// different APIs we define.
@HostApi()
abstract class HostSmallApi {
@async
@ObjCSelector('echoString:')
String echo(String aString);

@async
void voidVoid();
}

/// A simple API called in some unit tests.
//
// This is separate from FlutterIntegrationCoreApi to allow for incrementally
// moving from the previous fragmented unit test structure to something more
// unified.
// TODO(stuartmorgan): Restructure the unit tests to reduce the number of
// different APIs we define.
@FlutterApi()
abstract class FlutterSmallApi {
@ObjCSelector('echoWrappedList:')
@SwiftFunction('echo(_:)')
TestMessage echoWrappedList(TestMessage msg);

@ObjCSelector('echoString:')
@SwiftFunction('echo(string:)')
String echoString(String aString);
}

/// A data class containing a List, used in unit tests.
// TODO(stuartmorgan): Evaluate whether these unit tests are still useful; see
// TODOs above about restructuring.
class TestMessage {
// ignore: always_specify_types, strict_raw_type
List? testList;
}
57 changes: 57 additions & 0 deletions packages/pigeon/pigeons/core_tests_small_apis.dart

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.

Let's call this core_tests_small_apis.dart

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of 'core_tests.dart';

/// An API that can be implemented for minimal, compile-only tests.
//
// This is also here to test that multiple host APIs can be generated
// successfully in all languages (e.g., in Java where it requires having a
// wrapper class).
@HostApi()
abstract class HostTrivialApi {
void noop();
}

/// A simple API implemented in some unit tests.
//
// This is separate from HostIntegrationCoreApi to avoid having to update a
// lot of unit tests every time we add something to the integration test API.
// TODO(stuartmorgan): Restructure the unit tests to reduce the number of
// different APIs we define.
@HostApi()
abstract class HostSmallApi {
@async
@ObjCSelector('echoString:')
String echo(String aString);

@async
void voidVoid();
}

/// A simple API called in some unit tests.
//
// This is separate from FlutterIntegrationCoreApi to allow for incrementally
// moving from the previous fragmented unit test structure to something more
// unified.
// TODO(stuartmorgan): Restructure the unit tests to reduce the number of
// different APIs we define.
@FlutterApi()
abstract class FlutterSmallApi {
@ObjCSelector('echoWrappedList:')
@SwiftFunction('echo(_:)')
TestMessage echoWrappedList(TestMessage msg);

@ObjCSelector('echoString:')
@SwiftFunction('echo(string:)')
String echoString(String aString);
}

/// A data class containing a List, used in unit tests.
// TODO(stuartmorgan): Evaluate whether these unit tests are still useful; see
// TODOs above about restructuring.
class TestMessage {
// ignore: always_specify_types, strict_raw_type
List? testList;
}
2 changes: 1 addition & 1 deletion packages/pigeon/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 27.1.0 # This must match the version in lib/src/generator_tools.dart
version: 27.2.0 # This must match the version in lib/src/generator_tools.dart

environment:
sdk: ^3.10.0
Expand Down
Loading