diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index e70fe00a56a4..916106ed846e 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 27.3.0 + +* Adds support for sharing constants across platforms. + ## 27.2.0 * Adds support for empty data classes. diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md index 1029d51f3be9..126c20df46ae 100644 --- a/packages/pigeon/README.md +++ b/packages/pigeon/README.md @@ -1,3 +1,4 @@ + # Pigeon Pigeon is a code generator tool to make communication between Flutter and the @@ -82,6 +83,20 @@ the threading model for handling HostApi methods can be selected with the Host and Flutter APIs now support the ability to provide a unique message channel suffix string to the api to allow for multiple instances to be created and operate in parallel. +### Constants + +Pigeon supports generating top-level constants in the generated files. Constants can be defined at the top level of the Pigeon file: + + +```dart +const String aStringConstant = 'stringConstantValue'; +const int anIntConstant = 42; +const double aDoubleConstant = 3.14; +const bool aBoolConstant = true; +``` + +These constants will be translated into static constants or final variables in the target languages (e.g., `public static final` in Java, `let` in Swift, `const` in Dart, etc.). Only `String`, `int`, `double`, and `bool` constant types are supported. + ## Usage 1) Add pigeon as a `dev_dependency`. diff --git a/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java b/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java index 75a676627eca..ae6eb680e1d2 100644 --- a/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java +++ b/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java @@ -27,6 +27,12 @@ /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class Messages { + + public static final String aStringConstant = "stringConstantValue"; + public static final long anIntConstant = 42L; + public static final double aDoubleConstant = 3.14; + public static final boolean aBoolConstant = true; + static boolean pigeonDoubleEquals(double a, double b) { // Normalize -0.0 to 0.0 and handle NaN equality. return (a == 0.0 ? 0.0 : a) == (b == 0.0 ? 0.0 : b) || (Double.isNaN(a) && Double.isNaN(b)); diff --git a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt index e81766320f31..28fa69ce42b6 100644 --- a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt +++ b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt @@ -13,6 +13,11 @@ import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +const val aStringConstant: String = "stringConstantValue" +const val anIntConstant: Long = 42L +const val aDoubleConstant: Double = 3.14 +const val aBoolConstant: Boolean = true + private object MessagesPigeonUtils { fun createConnectionError(channelName: String): FlutterError { diff --git a/packages/pigeon/example/app/ios/Runner/Messages.g.swift b/packages/pigeon/example/app/ios/Runner/Messages.g.swift index 686f51fcb553..59f44d8d0094 100644 --- a/packages/pigeon/example/app/ios/Runner/Messages.g.swift +++ b/packages/pigeon/example/app/ios/Runner/Messages.g.swift @@ -14,6 +14,11 @@ import Foundation #error("Unsupported platform.") #endif +public let aStringConstant: String = "stringConstantValue" +public let anIntConstant: Int64 = 42 +public let aDoubleConstant: Double = 3.14 +public let aBoolConstant: Bool = true + /// Error class for passing custom error details to Dart side. final class PigeonError: Error { let code: String diff --git a/packages/pigeon/example/app/lib/src/messages.g.dart b/packages/pigeon/example/app/lib/src/messages.g.dart index 10f648a9f714..573e95b06106 100644 --- a/packages/pigeon/example/app/lib/src/messages.g.dart +++ b/packages/pigeon/example/app/lib/src/messages.g.dart @@ -12,6 +12,11 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; +const String aStringConstant = 'stringConstantValue'; +const int anIntConstant = 42; +const double aDoubleConstant = 3.14; +const bool aBoolConstant = true; + Object? _extractReplyValueOrThrow( List? replyList, String channelName, { diff --git a/packages/pigeon/example/app/linux/messages.g.h b/packages/pigeon/example/app/linux/messages.g.h index 45a903dbb4c2..38c85f4d1eda 100644 --- a/packages/pigeon/example/app/linux/messages.g.h +++ b/packages/pigeon/example/app/linux/messages.g.h @@ -11,6 +11,11 @@ G_BEGIN_DECLS +#define PIGEON_EXAMPLE_PACKAGE_A_STRING_CONSTANT "stringConstantValue" +#define PIGEON_EXAMPLE_PACKAGE_AN_INT_CONSTANT 42 +#define PIGEON_EXAMPLE_PACKAGE_A_DOUBLE_CONSTANT 3.14 +#define PIGEON_EXAMPLE_PACKAGE_A_BOOL_CONSTANT TRUE + /** * PigeonExamplePackageCode: * PIGEON_EXAMPLE_PACKAGE_CODE_ONE: diff --git a/packages/pigeon/example/app/macos/Runner/messages.g.h b/packages/pigeon/example/app/macos/Runner/messages.g.h index 3f08b096986f..c717742660c6 100644 --- a/packages/pigeon/example/app/macos/Runner/messages.g.h +++ b/packages/pigeon/example/app/macos/Runner/messages.g.h @@ -13,6 +13,11 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const PGNAStringConstant; +extern const NSInteger PGNAnIntConstant; +extern const double PGNADoubleConstant; +extern const BOOL PGNABoolConstant; + typedef NS_ENUM(NSUInteger, PGNCode) { PGNCodeOne = 0, PGNCodeTwo = 1, diff --git a/packages/pigeon/example/app/macos/Runner/messages.g.m b/packages/pigeon/example/app/macos/Runner/messages.g.m index 10334a87e855..606315747e5c 100644 --- a/packages/pigeon/example/app/macos/Runner/messages.g.m +++ b/packages/pigeon/example/app/macos/Runner/messages.g.m @@ -12,6 +12,10 @@ @import Flutter; #endif +NSString *const PGNAStringConstant = @"stringConstantValue"; +const NSInteger PGNAnIntConstant = 42; +const double PGNADoubleConstant = 3.14; +const BOOL PGNABoolConstant = YES; static BOOL __attribute__((unused)) FLTPigeonDeepEquals(id _Nullable a, id _Nullable b) { if (a == b) { return YES; diff --git a/packages/pigeon/example/app/pigeons/messages.dart b/packages/pigeon/example/app/pigeons/messages.dart index 585fe650e3e7..48bbc026c534 100644 --- a/packages/pigeon/example/app/pigeons/messages.dart +++ b/packages/pigeon/example/app/pigeons/messages.dart @@ -63,3 +63,10 @@ abstract class MessageFlutterApi { } // #enddocregion flutter-definitions + +// #docregion constants +const String aStringConstant = 'stringConstantValue'; +const int anIntConstant = 42; +const double aDoubleConstant = 3.14; +const bool aBoolConstant = true; +// #enddocregion constants diff --git a/packages/pigeon/example/app/windows/runner/messages.g.h b/packages/pigeon/example/app/windows/runner/messages.g.h index 2ad87b1205df..22756881cc41 100644 --- a/packages/pigeon/example/app/windows/runner/messages.g.h +++ b/packages/pigeon/example/app/windows/runner/messages.g.h @@ -20,6 +20,11 @@ namespace pigeon_example { // Generated class from Pigeon. +inline constexpr const char* aStringConstant = "stringConstantValue"; +inline constexpr int64_t anIntConstant = 42; +inline constexpr double aDoubleConstant = 3.14; +inline constexpr bool aBoolConstant = true; + class FlutterError { public: explicit FlutterError(const std::string& code) : code_(code) {} diff --git a/packages/pigeon/lib/src/ast.dart b/packages/pigeon/lib/src/ast.dart index 0ae00783b7df..8b23bef2e73f 100644 --- a/packages/pigeon/lib/src/ast.dart +++ b/packages/pigeon/lib/src/ast.dart @@ -785,6 +785,38 @@ class EnumMember extends Node { } } +/// Represents a constant. +class Constant extends Node { + /// Parametric constructor for [Constant]. + Constant({ + required this.name, + required this.type, + required this.value, + this.offset, + this.documentationComments = const [], + }); + + /// The name of the constant. + final String name; + + /// The type of the constant. + final TypeDeclaration type; + + /// The value of the constant. + final Object value; + + /// The offset in the source file where the constant appears. + final int? offset; + + /// List of documentation comments, separated by line. + final List documentationComments; + + @override + String toString() { + return '(Constant name:$name type:$type value:$value documentationComments:$documentationComments)'; + } +} + /// Top-level node for the AST. class Root extends Node { /// Parametric constructor for [Root]. @@ -792,6 +824,7 @@ class Root extends Node { required this.classes, required this.apis, required this.enums, + this.constants = const [], this.containsHostApi = false, this.containsFlutterApi = false, this.containsProxyApi = false, @@ -800,7 +833,7 @@ class Root extends Node { /// Factory function for generating an empty root, usually used when early errors are encountered. factory Root.makeEmpty() { - return Root(apis: [], classes: [], enums: []); + return Root(apis: [], classes: [], enums: [], constants: []); } // TODO(tarrinneal): Ensure classes are sorted in topological dependency order; see @@ -814,6 +847,9 @@ class Root extends Node { /// All of the enums contained in the AST. List enums; + /// All of the constants contained in the AST. + List constants; + /// Whether the root has any Host API definitions. bool containsHostApi; @@ -835,6 +871,6 @@ class Root extends Node { @override String toString() { - return '(Root classes:$classes apis:$apis enums:$enums)'; + return '(Root classes:$classes apis:$apis enums:$enums constants:$constants)'; } } diff --git a/packages/pigeon/lib/src/cpp/cpp_generator.dart b/packages/pigeon/lib/src/cpp/cpp_generator.dart index 36bc99653a12..156643c2aa47 100644 --- a/packages/pigeon/lib/src/cpp/cpp_generator.dart +++ b/packages/pigeon/lib/src/cpp/cpp_generator.dart @@ -817,6 +817,30 @@ $friendLines '''); } + @override + void writeConstants( + InternalCppOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, _docCommentSpec); + final String type = constant.type.baseName; + if (type == 'String') { + final String escaped = escapeStringDoubleQuotes(constant.value.toString()); + indent.writeln('inline constexpr const char* ${constant.name} = "$escaped";'); + } else { + final String cppType = _baseCppTypeForBuiltinDartType(constant.type) ?? 'auto'; + indent.writeln('inline constexpr $cppType ${constant.name} = ${constant.value};'); + } + } + } + @override void writeCloseNamespace( InternalCppOptions generatorOptions, diff --git a/packages/pigeon/lib/src/dart/dart_generator.dart b/packages/pigeon/lib/src/dart/dart_generator.dart index 94fe726ea933..58355db8a448 100644 --- a/packages/pigeon/lib/src/dart/dart_generator.dart +++ b/packages/pigeon/lib/src/dart/dart_generator.dart @@ -186,6 +186,65 @@ class DartGenerator extends StructuredGenerator { indent.writeln("import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;"); } + @override + void writeConstants( + InternalDartOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, docCommentSpec); + final String formattedValue = _formatValue(constant.type.baseName, constant.value); + indent.writeln('const ${constant.type.baseName} ${constant.name} = $formattedValue;'); + } + } + + String _formatValue(String type, Object value) => + type == 'String' ? _makeDartStringLiteral(value.toString()) : value.toString(); + + String _makeDartStringLiteral(String valStr) { + final bool hasSpecial = + valStr.contains(r'\') || + valStr.contains(r'$') || + valStr.contains('\n') || + valStr.contains('\r'); + + if (!hasSpecial) { + if (!valStr.contains("'")) { + return "'$valStr'"; + } + if (!valStr.contains('"')) { + return '"$valStr"'; + } + return "'${escapeStringSingleQuotes(valStr)}'"; + } + + if (!valStr.contains('\n') && !valStr.contains('\r')) { + if (!valStr.contains("'")) { + return "r'$valStr'"; + } + if (!valStr.contains('"')) { + return 'r"$valStr"'; + } + return "'${escapeStringSingleQuotes(valStr)}'"; + } + + if (!valStr.contains("'''")) { + return "r'''$valStr'''"; + } + if (!valStr.contains('"""')) { + return 'r"""$valStr"""'; + } + + final String escaped = escapeStringSingleQuotes(valStr); + return "'$escaped'"; + } + @override void writeEnum( InternalDartOptions generatorOptions, diff --git a/packages/pigeon/lib/src/generator.dart b/packages/pigeon/lib/src/generator.dart index f1e2568749f9..f263bda3f57f 100644 --- a/packages/pigeon/lib/src/generator.dart +++ b/packages/pigeon/lib/src/generator.dart @@ -37,6 +37,8 @@ abstract class StructuredGenerator extends Generator< writeOpenNamespace(generatorOptions, root, indent, dartPackageName: dartPackageName); + writeConstants(generatorOptions, root, indent, dartPackageName: dartPackageName); + writeGeneralUtilities(generatorOptions, root, indent, dartPackageName: dartPackageName); if (root.apis.any((Api api) => api is AstProxyApi)) { @@ -106,6 +108,16 @@ abstract class StructuredGenerator extends Generator< required String dartPackageName, }) {} + /// Writes all constants to [indent]. + /// + /// This method is not required, and does not need to be overridden. + void writeConstants( + T generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) {} + /// Writes all enums to [indent]. /// /// Can be overridden to add extra code before/after enums. diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index 1f3f71af1f12..0438945a8edc 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 = '27.2.0'; +const String pigeonVersion = '27.3.0'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; @@ -871,3 +871,22 @@ bool isCollectionType(TypeDeclaration type) { !type.isProxyApi && (type.baseName.contains('List') || type.baseName == 'Map'); } + +/// Escapes special characters in a string for use in double-quoted string literals. +String escapeStringDoubleQuotes(String value) { + return value + .replaceAll(r'\', r'\\') + .replaceAll('"', r'\"') + .replaceAll('\n', r'\n') + .replaceAll('\r', r'\r'); +} + +/// Escapes special characters in a string for use in single-quoted string literals. +String escapeStringSingleQuotes(String value) { + return value + .replaceAll(r'\', r'\\') + .replaceAll("'", r"\'") + .replaceAll('\n', r'\n') + .replaceAll('\r', r'\r') + .replaceAll(r'$', r'\$'); +} diff --git a/packages/pigeon/lib/src/gobject/gobject_generator.dart b/packages/pigeon/lib/src/gobject/gobject_generator.dart index ce01cfccdfa1..cf643c2b9295 100644 --- a/packages/pigeon/lib/src/gobject/gobject_generator.dart +++ b/packages/pigeon/lib/src/gobject/gobject_generator.dart @@ -198,6 +198,36 @@ class GObjectHeaderGenerator extends StructuredGenerator indent.writeln('G_BEGIN_DECLS'); } + @override + void writeConstants( + InternalGObjectOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + final String module = _getModule(generatorOptions, dartPackageName); + final String moduleScreaming = toScreamingSnakeCase(module); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, _docCommentSpec); + final String constantName = toScreamingSnakeCase(constant.name); + final String type = constant.type.baseName; + final String valueStr; + if (type == 'String') { + final String escaped = escapeStringDoubleQuotes(constant.value.toString()); + valueStr = '"$escaped"'; + } else if (type == 'bool') { + valueStr = (constant.value as bool) ? 'TRUE' : 'FALSE'; + } else { + valueStr = constant.value.toString(); + } + indent.writeln('#define ${moduleScreaming}_$constantName $valueStr'); + } + } + @override void writeEnum( InternalGObjectOptions generatorOptions, diff --git a/packages/pigeon/lib/src/java/java_generator.dart b/packages/pigeon/lib/src/java/java_generator.dart index e0854b438d9d..b72e1a5f1c1c 100644 --- a/packages/pigeon/lib/src/java/java_generator.dart +++ b/packages/pigeon/lib/src/java/java_generator.dart @@ -200,11 +200,34 @@ class JavaGenerator extends StructuredGenerator { } indent.writeln('public class ${generatorOptions.className!} {'); indent.inc(); - _writeNumberHelpers(indent); - _writeDeepEquals(indent); - _writeDeepHashCode(indent); } + @override + void writeConstants( + InternalJavaOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, _docCommentSpec); + final String javaType = + _javaTypeForBuiltinDartType(constant.type, primitive: true) ?? 'Object'; + final String formattedValue = _formatJavaValue(constant.type.baseName, constant.value); + indent.writeln('public static final $javaType ${constant.name} = $formattedValue;'); + } + } + + String _formatJavaValue(String type, Object value) => switch (type) { + 'String' => '"${escapeStringDoubleQuotes(value.toString())}"', + 'int' => '${value}L', + _ => value.toString(), + }; + @override void writeEnum( InternalJavaOptions generatorOptions, @@ -1326,6 +1349,10 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { Indent indent, { required String dartPackageName, }) { + indent.newln(); + _writeNumberHelpers(indent); + _writeDeepEquals(indent); + _writeDeepHashCode(indent); indent.newln(); _writeErrorClass(indent); if (root.containsHostApi) { @@ -1391,7 +1418,17 @@ String _javaTypeForBuiltinGenericDartType(TypeDeclaration type, int numberTypeAr } } -String? _javaTypeForBuiltinDartType(TypeDeclaration type) { +String? _javaTypeForBuiltinDartType(TypeDeclaration type, {bool primitive = false}) { + if (primitive) { + switch (type.baseName) { + case 'bool': + return 'boolean'; + case 'int': + return 'long'; + case 'double': + return 'double'; + } + } const javaTypeForDartTypeMap = { 'bool': 'Boolean', 'int': 'Long', diff --git a/packages/pigeon/lib/src/kotlin/kotlin_generator.dart b/packages/pigeon/lib/src/kotlin/kotlin_generator.dart index 9354fb3928ee..646a5cc6481a 100644 --- a/packages/pigeon/lib/src/kotlin/kotlin_generator.dart +++ b/packages/pigeon/lib/src/kotlin/kotlin_generator.dart @@ -239,6 +239,31 @@ class KotlinGenerator extends StructuredGenerator { } } + @override + void writeConstants( + InternalKotlinOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, _docCommentSpec); + final String kotlinType = _kotlinTypeForBuiltinDartType(constant.type) ?? 'Any'; + final String formattedValue = _formatKotlinValue(constant.type.baseName, constant.value); + indent.writeln('const val ${constant.name}: $kotlinType = $formattedValue'); + } + } + + String _formatKotlinValue(String type, Object value) => switch (type) { + 'String' => '"${escapeStringDoubleQuotes(value.toString()).replaceAll(r'$', r'\$')}"', + 'int' => '${value}L', + _ => value.toString(), + }; + @override void writeEnum( InternalKotlinOptions generatorOptions, diff --git a/packages/pigeon/lib/src/objc/objc_generator.dart b/packages/pigeon/lib/src/objc/objc_generator.dart index b922a2f702e1..6c2311a862ce 100644 --- a/packages/pigeon/lib/src/objc/objc_generator.dart +++ b/packages/pigeon/lib/src/objc/objc_generator.dart @@ -206,6 +206,32 @@ class ObjcHeaderGenerator extends StructuredGenerator { indent.writeln('NS_ASSUME_NONNULL_BEGIN'); } + @override + void writeConstants( + InternalObjcOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, _docCommentSpec); + final String constantName = _constantName(generatorOptions.prefix, constant.name); + final String type = constant.type.baseName; + if (type == 'String') { + indent.writeln('extern NSString *const $constantName;'); + } else if (type == 'bool') { + indent.writeln('extern const BOOL $constantName;'); + } else { + final _ObjcType objcType = _objcTypeForDartType(generatorOptions.prefix, constant.type); + indent.writeln('extern const $objcType $constantName;'); + } + } + } + @override void writeEnum( InternalObjcOptions generatorOptions, @@ -467,8 +493,33 @@ class ObjcSourceGenerator extends StructuredGenerator { indent.writeln('@import Flutter;'); indent.writeln('#endif'); indent.newln(); - _writeDeepEquals(indent); - _writeDeepHash(indent); + } + + @override + void writeConstants( + InternalObjcOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + final String constantName = _constantName(generatorOptions.prefix, constant.name); + final String type = constant.type.baseName; + if (type == 'String') { + final String escaped = escapeStringDoubleQuotes(constant.value.toString()); + indent.writeln('NSString *const $constantName = @"$escaped";'); + } else if (type == 'bool') { + final boolVal = (constant.value as bool) ? 'YES' : 'NO'; + indent.writeln('const BOOL $constantName = $boolVal;'); + } else { + final _ObjcType objcType = _objcTypeForDartType(generatorOptions.prefix, constant.type); + indent.writeln('const $objcType $constantName = ${constant.value};'); + } + } } @override @@ -1007,6 +1058,9 @@ if (self.wrapped == nil) { Indent indent, { required String dartPackageName, }) { + _writeDeepEquals(indent); + _writeDeepHash(indent); + indent.newln(); if (root.containsHostApi) { _writeWrapError(indent); indent.newln(); @@ -1437,6 +1491,15 @@ String _className(String? prefix, String className) { } } +/// Calculates the ObjC constant name, converting to UpperCamelCase when prefixed. +String _constantName(String? prefix, String constantName) { + if (prefix != null && prefix.isNotEmpty) { + return '$prefix${toUpperCamelCase(constantName)}'; + } else { + return constantName; + } +} + /// Calculates callback block signature for async methods. String _callbackForType(TypeDeclaration type, _ObjcType objcType, InternalObjcOptions options) { if (type.isVoid) { diff --git a/packages/pigeon/lib/src/pigeon_lib_internal.dart b/packages/pigeon/lib/src/pigeon_lib_internal.dart index 756451173d34..d1a12fd93988 100644 --- a/packages/pigeon/lib/src/pigeon_lib_internal.dart +++ b/packages/pigeon/lib/src/pigeon_lib_internal.dart @@ -1094,6 +1094,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { final List _apis = []; final List _enums = []; final List _classes = []; + final List _constants = []; final List _errors = []; /// Input file location. @@ -1157,6 +1158,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { apis: _apis, classes: _classes, enums: referencedEnums, + constants: _constants, containsHostApi: containsHostApi, containsFlutterApi: containsFlutterApi, containsProxyApi: containsProxyApi, @@ -1216,6 +1218,38 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { final List validateErrors = _validateAst(completeRoot, source); totalErrors.addAll(validateErrors); + final allowedValueValidators = { + 'String': (Object? v) => v is String, + 'int': (Object? v) => v is int, + 'double': (Object? v) => v is num, + 'bool': (Object? v) => v is bool, + }; + for (final Constant constant in _constants) { + final String typeName = constant.type.baseName; + final bool Function(Object?)? validator = allowedValueValidators[typeName]; + if (validator == null) { + totalErrors.add( + Error( + message: + 'Unsupported constant type: "$typeName". Only String, int, double, and bool are supported.', + lineNumber: constant.offset != null + ? calculateLineNumber(source, constant.offset!) + : null, + ), + ); + } else if (!validator(constant.value)) { + totalErrors.add( + Error( + message: + 'Constant "${constant.name}" type is $typeName but value is ${constant.value.runtimeType}.', + lineNumber: constant.offset != null + ? calculateLineNumber(source, constant.offset!) + : null, + ), + ); + } + } + return ParseResults( root: totalErrors.isEmpty ? completeRoot @@ -1354,6 +1388,133 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { return null; } + @override + Object? visitTopLevelVariableDeclaration(dart_ast.TopLevelVariableDeclaration node) { + if (node.variables.isConst) { + final dart_ast.TypeAnnotation? typeAnnotation = node.variables.type; + if (typeAnnotation == null) { + _errors.add( + Error( + message: 'Top-level constants must have an explicit type annotation.', + lineNumber: calculateLineNumber(source, node.offset), + ), + ); + return null; + } + if (typeAnnotation is! dart_ast.NamedType) { + _errors.add( + Error( + message: 'Top-level constants must have a named type annotation.', + lineNumber: calculateLineNumber(source, node.offset), + ), + ); + return null; + } + for (final dart_ast.VariableDeclaration variable in node.variables.variables) { + final dart_ast.Expression? initializer = variable.initializer; + if (initializer == null) { + _errors.add( + Error( + message: 'Top-level constant "${variable.name.lexeme}" must have an initializer.', + lineNumber: calculateLineNumber(source, variable.offset), + ), + ); + continue; + } + + Object? value = _evaluateExpression(initializer); + if (value == null) { + continue; + } + + if (_getNamedTypeQualifiedName(typeAnnotation) == 'double' && value is num) { + value = value.toDouble(); + } + + final type = TypeDeclaration( + baseName: _getNamedTypeQualifiedName(typeAnnotation), + isNullable: typeAnnotation.question != null, + typeArguments: _typeAnnotationsToTypeArguments(typeAnnotation.typeArguments), + ); + + _constants.add( + Constant( + name: variable.name.lexeme, + type: type, + value: value, + offset: variable.offset, + documentationComments: _documentationCommentsParser(node.documentationComment?.tokens), + ), + ); + } + } + node.visitChildren(this); + return null; + } + + Object? _evaluateExpression(dart_ast.Expression expression) { + if (expression is dart_ast.SimpleStringLiteral) { + return expression.value; + } else if (expression is dart_ast.IntegerLiteral) { + return expression.value!; + } else if (expression is dart_ast.DoubleLiteral) { + return expression.value; + } else if (expression is dart_ast.BooleanLiteral) { + return expression.value; + } else if (expression is dart_ast.PrefixExpression) { + final Object? operandValue = _evaluateExpression(expression.operand); + if (operandValue == null) { + return null; + } + final String operator = expression.operator.lexeme; + if (operator == '-') { + if (operandValue is int) { + return -operandValue; + } else if (operandValue is double) { + return -operandValue; + } + } else if (operator == '!') { + if (operandValue is bool) { + return !operandValue; + } + } + _errors.add( + Error( + message: 'Unsupported prefix operator "$operator" on type "${operandValue.runtimeType}".', + lineNumber: calculateLineNumber(source, expression.offset), + ), + ); + return null; + } else if (expression is dart_ast.AdjacentStrings) { + final buffer = StringBuffer(); + for (final dart_ast.StringLiteral literal in expression.strings) { + final Object? val = _evaluateExpression(literal); + if (val is! String) { + return null; + } + buffer.write(val); + } + return buffer.toString(); + } else if (expression is dart_ast.StringInterpolation) { + _errors.add( + Error( + message: 'String interpolation is not supported in Pigeon constants.', + lineNumber: calculateLineNumber(source, expression.offset), + ), + ); + return null; + } else { + _errors.add( + Error( + message: + 'Unsupported expression type ${expression.runtimeType} for constant initializer.', + lineNumber: calculateLineNumber(source, expression.offset), + ), + ); + return null; + } + } + @override Object? visitAnnotation(dart_ast.Annotation node) { if (node.name.name == 'ConfigurePigeon') { diff --git a/packages/pigeon/lib/src/swift/swift_generator.dart b/packages/pigeon/lib/src/swift/swift_generator.dart index 248ce1a18abc..200460ff145d 100644 --- a/packages/pigeon/lib/src/swift/swift_generator.dart +++ b/packages/pigeon/lib/src/swift/swift_generator.dart @@ -223,6 +223,34 @@ class SwiftGenerator extends StructuredGenerator { #endif'''); } + @override + void writeConstants( + InternalSwiftOptions generatorOptions, + Root root, + Indent indent, { + required String dartPackageName, + }) { + if (root.constants.isEmpty) { + return; + } + indent.newln(); + for (final Constant constant in root.constants) { + addDocumentationComments(indent, constant.documentationComments, _docCommentSpec); + final String swiftType = _swiftTypeForDartType(constant.type); + final String formattedValue = _formatSwiftValue(constant.type.baseName, constant.value); + indent.writeln('public let ${constant.name}: $swiftType = $formattedValue'); + } + } + + String _formatSwiftValue(String type, Object value) { + if (type == 'String') { + final String escaped = escapeStringDoubleQuotes(value.toString()); + return '"$escaped"'; + } else { + return value.toString(); + } + } + @override void writeEnum( InternalSwiftOptions generatorOptions, diff --git a/packages/pigeon/pigeons/core_tests.dart b/packages/pigeon/pigeons/core_tests.dart index 381c3d0b5f23..385e9a258a02 100644 --- a/packages/pigeon/pigeons/core_tests.dart +++ b/packages/pigeon/pigeons/core_tests.dart @@ -4,6 +4,12 @@ import 'package:pigeon/pigeon.dart'; +const String aStringConstant = 'stringConstantValue'; +const String aStringConstantWithEscapes = r'''string\'\"\$ConstantValue'''; +const int anIntConstant = 42; +const double aDoubleConstant = 3.14; +const bool aBoolConstant = true; + enum AnEnum { one, two, three, fortyTwo, fourHundredTwentyTwo } // Enums require special logic, having multiple ensures that the logic can be diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java index 868874c99ee2..547a2395c228 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java @@ -31,6 +31,13 @@ /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class CoreTests { + + public static final String aStringConstant = "stringConstantValue"; + public static final String aStringConstantWithEscapes = "string\\'\\\"\\$ConstantValue"; + public static final long anIntConstant = 42L; + public static final double aDoubleConstant = 3.14; + public static final boolean aBoolConstant = true; + static boolean pigeonDoubleEquals(double a, double b) { // Normalize -0.0 to 0.0 and handle NaN equality. return (a == 0.0 ? 0.0 : a) == (b == 0.0 ? 0.0 : b) || (Double.isNaN(a) && Double.isNaN(b)); diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/DataClassMethodsTest.java b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/DataClassMethodsTest.java index 7b0c28355014..4c680ff923d3 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/DataClassMethodsTest.java +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/DataClassMethodsTest.java @@ -405,4 +405,13 @@ public void nestedZeroArrayEquality() { assertEquals(a, b); assertEquals(a.hashCode(), b.hashCode()); } + + @Test + public void testConstants() { + assertEquals("stringConstantValue", CoreTests.aStringConstant); + assertEquals("string\\'\\\"\\$ConstantValue", CoreTests.aStringConstantWithEscapes); + assertEquals(42L, CoreTests.anIntConstant); + assertEquals(3.14, CoreTests.aDoubleConstant, 0.0001); + assertTrue(CoreTests.aBoolConstant); + } } diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/CoreTests.gen.m b/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/CoreTests.gen.m index 60799e5a6a9d..26d7d40054b7 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/CoreTests.gen.m +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/CoreTests.gen.m @@ -13,6 +13,11 @@ @import Flutter; #endif +NSString *const FLTAStringConstant = @"stringConstantValue"; +NSString *const FLTAStringConstantWithEscapes = @"string\\'\\\"\\$ConstantValue"; +const NSInteger FLTAnIntConstant = 42; +const double FLTADoubleConstant = 3.14; +const BOOL FLTABoolConstant = YES; static BOOL __attribute__((unused)) FLTPigeonDeepEquals(id _Nullable a, id _Nullable b) { if (a == b) { return YES; diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/include/alternate_language_test_plugin/CoreTests.gen.h b/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/include/alternate_language_test_plugin/CoreTests.gen.h index 6dc4e807620d..9ec9e3a03eca 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/include/alternate_language_test_plugin/CoreTests.gen.h +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/darwin/alternate_language_test_plugin/Sources/alternate_language_test_plugin/include/alternate_language_test_plugin/CoreTests.gen.h @@ -14,6 +14,12 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const FLTAStringConstant; +extern NSString *const FLTAStringConstantWithEscapes; +extern const NSInteger FLTAnIntConstant; +extern const double FLTADoubleConstant; +extern const BOOL FLTABoolConstant; + typedef NS_ENUM(NSUInteger, FLTAnEnum) { FLTAnEnumOne = 0, FLTAnEnumTwo = 1, diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/RunnerTests/DataClassMethodsTest.m b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/RunnerTests/DataClassMethodsTest.m index 68e655ec0dca..7f9d2b004ac0 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/RunnerTests/DataClassMethodsTest.m +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/RunnerTests/DataClassMethodsTest.m @@ -253,4 +253,12 @@ - (void)testNSNullPropertyEquality { XCTAssertEqual(a.hash, b.hash); } +- (void)testConstants { + XCTAssertEqualObjects(FLTAStringConstant, @"stringConstantValue"); + XCTAssertEqualObjects(FLTAStringConstantWithEscapes, @"string\\'\\\"\\$ConstantValue"); + XCTAssertEqual(FLTAnIntConstant, 42); + XCTAssertEqual(FLTADoubleConstant, 3.14); + XCTAssertEqual(FLTABoolConstant, YES); +} + @end diff --git a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart index 5c24c9716589..27a2068871ac 100644 --- a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart +++ b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/integration_tests.dart @@ -2798,6 +2798,14 @@ void runPigeonIntegrationTests(TargetGenerator targetGenerator) { await completer1.future; await completer2.future; }, skip: !eventChannelSupported.contains(targetGenerator)); + + testWidgets('constants are generated correctly', (WidgetTester _) async { + expect(aStringConstant, 'stringConstantValue'); + expect(aStringConstantWithEscapes, r'''string\'\"\$ConstantValue'''); + expect(anIntConstant, 42); + expect(aDoubleConstant, 3.14); + expect(aBoolConstant, true); + }); } class _FlutterApiTestImplementation implements FlutterIntegrationCoreApi { diff --git a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart index 7f0a46e303fb..1fb7bb6fb76a 100644 --- a/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart +++ b/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/core_tests.gen.dart @@ -13,6 +13,12 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; +const String aStringConstant = 'stringConstantValue'; +const String aStringConstantWithEscapes = 'string\\\'\\"\\\$ConstantValue'; +const int anIntConstant = 42; +const double aDoubleConstant = 3.14; +const bool aBoolConstant = true; + Object? _extractReplyValueOrThrow( List? replyList, String channelName, { diff --git a/packages/pigeon/platform_tests/shared_test_plugin_code/test/data_class_methods_test.dart b/packages/pigeon/platform_tests/shared_test_plugin_code/test/data_class_methods_test.dart index 1327fbc2b333..2af927ba3120 100644 --- a/packages/pigeon/platform_tests/shared_test_plugin_code/test/data_class_methods_test.dart +++ b/packages/pigeon/platform_tests/shared_test_plugin_code/test/data_class_methods_test.dart @@ -200,6 +200,14 @@ void main() { ); }); + test('constants', () { + expect(aStringConstant, 'stringConstantValue'); + expect(aStringConstantWithEscapes, r'''string\'\"\$ConstantValue'''); + expect(anIntConstant, 42); + expect(aDoubleConstant, 3.14); + expect(aBoolConstant, true); + }); + test('AnEmptyClass basic methods', () { final a = AnEmptyClass(); final b = AnEmptyClass(); diff --git a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt index 25ff604c6dea..576574f882a3 100644 --- a/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt +++ b/packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/CoreTests.gen.kt @@ -16,6 +16,12 @@ import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +const val aStringConstant: String = "stringConstantValue" +const val aStringConstantWithEscapes: String = "string\\'\\\"\\\$ConstantValue" +const val anIntConstant: Long = 42L +const val aDoubleConstant: Double = 3.14 +const val aBoolConstant: Boolean = true + private object CoreTestsPigeonUtils { fun createConnectionError(channelName: String): FlutterError { diff --git a/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/DataClassMethodsTest.kt b/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/DataClassMethodsTest.kt index ab9a69d91208..72eea8565991 100644 --- a/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/DataClassMethodsTest.kt +++ b/packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/DataClassMethodsTest.kt @@ -303,4 +303,13 @@ internal class DataClassMethodsTest { "AllNullableTypes(aNullableBool=false, aNullableInt=1234, aNullableInt64=null, aNullableDouble=2.0, aNullableByteArray=[1, 2, 3, 4], aNullable4ByteArray=[1, 2, 3, 4], aNullable8ByteArray=[1, 2, 3, 4], aNullableFloatArray=[0.5, 0.25, 1.5, 1.25], aNullableEnum=TWO, anotherNullableEnum=JUST_IN_CASE, aNullableString=hello, aNullableObject=0, allNullableTypes=null, list=[1, 2, 3], stringList=[string, another one], intList=[1, 2], doubleList=[1.1, 2.2], boolList=[true, false], enumList=[ONE, TWO], objectList=[1, 2, 3], listList=[[string, another one], [string, another one]], mapList=[{hello=1234}, {hello=1234}], recursiveClassList=null, map={hello=1234}, stringMap={hello=you}, intMap={1=0}, enumMap={ONE=FORTY_TWO, TWO=FOUR_HUNDRED_TWENTY_TWO}, objectMap={hello=1234}, listMap={1=[string, another one]}, mapMap={1={}}, recursiveClassMap=null)", everything.toString()) } + + @Test + fun testConstants() { + assertEquals("stringConstantValue", aStringConstant) + assertEquals("string\\'\\\"\\\$ConstantValue", aStringConstantWithEscapes) + assertEquals(42L, anIntConstant) + assertEquals(3.14, aDoubleConstant, 0.0) + assertEquals(true, aBoolConstant) + } } diff --git a/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift b/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift index 1afcdbd7453c..91af587825a1 100644 --- a/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift +++ b/packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/CoreTests.gen.swift @@ -15,6 +15,12 @@ import Foundation #error("Unsupported platform.") #endif +public let aStringConstant: String = "stringConstantValue" +public let aStringConstantWithEscapes: String = "string\\'\\\"\\$ConstantValue" +public let anIntConstant: Int64 = 42 +public let aDoubleConstant: Double = 3.14 +public let aBoolConstant: Bool = true + /// Error class for passing custom error details to Dart side. final class PigeonError: Error { let code: String diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/DataClassMethodsTests.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/DataClassMethodsTests.swift index 69360f095190..87122369f2d0 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/DataClassMethodsTests.swift +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/DataClassMethodsTests.swift @@ -383,4 +383,13 @@ struct DataClassMethodsTests { #expect(hashA == hashB) } + + @Test + func testConstants() { + #expect(aStringConstant == "stringConstantValue") + #expect(aStringConstantWithEscapes == "string\\'\\\"\\$ConstantValue") + #expect(anIntConstant == 42) + #expect(aDoubleConstant == 3.14) + #expect(aBoolConstant == true) + } } diff --git a/packages/pigeon/platform_tests/test_plugin/linux/pigeon/core_tests.gen.h b/packages/pigeon/platform_tests/test_plugin/linux/pigeon/core_tests.gen.h index d3de994f5927..6ca8abdf7c02 100644 --- a/packages/pigeon/platform_tests/test_plugin/linux/pigeon/core_tests.gen.h +++ b/packages/pigeon/platform_tests/test_plugin/linux/pigeon/core_tests.gen.h @@ -12,6 +12,13 @@ G_BEGIN_DECLS +#define CORE_TESTS_PIGEON_TEST_A_STRING_CONSTANT "stringConstantValue" +#define CORE_TESTS_PIGEON_TEST_A_STRING_CONSTANT_WITH_ESCAPES \ + "string\\'\\\"\\$ConstantValue" +#define CORE_TESTS_PIGEON_TEST_AN_INT_CONSTANT 42 +#define CORE_TESTS_PIGEON_TEST_A_DOUBLE_CONSTANT 3.14 +#define CORE_TESTS_PIGEON_TEST_A_BOOL_CONSTANT TRUE + /** * CoreTestsPigeonTestAnEnum: * PIGEON_INTEGRATION_TESTS_AN_ENUM_ONE: diff --git a/packages/pigeon/platform_tests/test_plugin/linux/test/data_class_methods_test.cc b/packages/pigeon/platform_tests/test_plugin/linux/test/data_class_methods_test.cc index 0346da708dec..8a03cb66da36 100644 --- a/packages/pigeon/platform_tests/test_plugin/linux/test/data_class_methods_test.cc +++ b/packages/pigeon/platform_tests/test_plugin/linux/test/data_class_methods_test.cc @@ -303,3 +303,12 @@ TEST(Equality, ToStringSnapshot) { g_free(str); } + +TEST(Constants, VerifyConstants) { + EXPECT_STREQ(CORE_TESTS_PIGEON_TEST_A_STRING_CONSTANT, "stringConstantValue"); + EXPECT_STREQ(CORE_TESTS_PIGEON_TEST_A_STRING_CONSTANT_WITH_ESCAPES, + "string\\'\\\"\\$ConstantValue"); + EXPECT_EQ(CORE_TESTS_PIGEON_TEST_AN_INT_CONSTANT, 42); + EXPECT_DOUBLE_EQ(CORE_TESTS_PIGEON_TEST_A_DOUBLE_CONSTANT, 3.14); + EXPECT_TRUE(CORE_TESTS_PIGEON_TEST_A_BOOL_CONSTANT); +} diff --git a/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h b/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h index 521cddded991..a93faee6c5a2 100644 --- a/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h +++ b/packages/pigeon/platform_tests/test_plugin/windows/pigeon/core_tests.gen.h @@ -23,6 +23,13 @@ class CoreTestsTest; // Generated class from Pigeon. +inline constexpr const char* aStringConstant = "stringConstantValue"; +inline constexpr const char* aStringConstantWithEscapes = + "string\\'\\\"\\$ConstantValue"; +inline constexpr int64_t anIntConstant = 42; +inline constexpr double aDoubleConstant = 3.14; +inline constexpr bool aBoolConstant = true; + class FlutterError { public: explicit FlutterError(const std::string& code) : code_(code) {} diff --git a/packages/pigeon/platform_tests/test_plugin/windows/test/data_class_methods_test.cpp b/packages/pigeon/platform_tests/test_plugin/windows/test/data_class_methods_test.cpp index dc1f6ad3f809..7dbe007b7d1e 100644 --- a/packages/pigeon/platform_tests/test_plugin/windows/test/data_class_methods_test.cpp +++ b/packages/pigeon/platform_tests/test_plugin/windows/test/data_class_methods_test.cpp @@ -153,5 +153,14 @@ TEST(SerializationTests, StreamOutputFullSnapshot) { "map_map: null, recursive_class_map: null)"); } +TEST(ConstantTests, VerifyConstants) { + EXPECT_EQ(std::string(aStringConstant), "stringConstantValue"); + EXPECT_EQ(std::string(aStringConstantWithEscapes), + "string\\'\\\"\\$ConstantValue"); + EXPECT_EQ(anIntConstant, 42); + EXPECT_DOUBLE_EQ(aDoubleConstant, 3.14); + EXPECT_EQ(aBoolConstant, true); +} + } // namespace test } // namespace test_plugin diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 8103bc0b2e72..af91648dcb59 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: 27.2.0 # This must match the version in lib/src/generator_tools.dart +version: 27.3.0 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.10.0 diff --git a/packages/pigeon/test/cpp_generator_test.dart b/packages/pigeon/test/cpp_generator_test.dart index fa65936598ca..cab245024fcb 100644 --- a/packages/pigeon/test/cpp_generator_test.dart +++ b/packages/pigeon/test/cpp_generator_test.dart @@ -2388,4 +2388,50 @@ void main() { expect(code, contains('size_t Input::Hash() const {')); } }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const generator = CppGenerator(); + final generatorOptions = OutputFileOptions( + fileType: FileType.header, + languageOptions: const InternalCppOptions( + headerIncludePath: 'foo.h', + cppHeaderOut: '', + cppSourceOut: '', + ), + ); + generator.generate(generatorOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('inline constexpr const char* stringConst = "hello";')); + expect(code, contains('inline constexpr int64_t intConst = 42;')); + expect(code, contains('inline constexpr double doubleConst = 3.14;')); + expect(code, contains('inline constexpr bool boolConst = true;')); + }); } diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index dce12d0c2034..8925c9aaf583 100644 --- a/packages/pigeon/test/dart_generator_test.dart +++ b/packages/pigeon/test/dart_generator_test.dart @@ -2067,4 +2067,47 @@ name: foobar expect(code, contains('String toString() {')); expect(code, contains(r"return 'Foobar(field1: $field1)';")); }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const generator = DartGenerator(); + generator.generate( + const InternalDartOptions(ignoreLints: false), + root, + sink, + dartPackageName: DEFAULT_PACKAGE_NAME, + ); + final code = sink.toString(); + expect(code, contains("const String stringConst = 'hello';")); + expect(code, contains('const int intConst = 42;')); + expect(code, contains('const double doubleConst = 3.14;')); + expect(code, contains('const bool boolConst = true;')); + }); } diff --git a/packages/pigeon/test/gobject_generator_test.dart b/packages/pigeon/test/gobject_generator_test.dart index 23e4f77f1244..566892ed5246 100644 --- a/packages/pigeon/test/gobject_generator_test.dart +++ b/packages/pigeon/test/gobject_generator_test.dart @@ -936,4 +936,50 @@ void main() { expect(code, contains('gchar* test_package_input_to_string(')); expect(code, contains('g_string_new("Input(");')); }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const generator = GObjectGenerator(); + final generatorOptions = OutputFileOptions( + fileType: FileType.header, + languageOptions: const InternalGObjectOptions( + headerIncludePath: 'foo.h', + gobjectHeaderOut: '', + gobjectSourceOut: '', + ), + ); + generator.generate(generatorOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('#define TEST_PACKAGE_STRING_CONST "hello"')); + expect(code, contains('#define TEST_PACKAGE_INT_CONST 42')); + expect(code, contains('#define TEST_PACKAGE_DOUBLE_CONST 3.14')); + expect(code, contains('#define TEST_PACKAGE_BOOL_CONST TRUE')); + }); } diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart index 6a93704be7a5..6d2a58420463 100644 --- a/packages/pigeon/test/java_generator_test.dart +++ b/packages/pigeon/test/java_generator_test.dart @@ -1582,4 +1582,43 @@ void main() { expect(code, contains('public String toString() {')); expect(code, contains('return "Foobar{" + "field1=" + field1 + "}";')); }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const javaOptions = InternalJavaOptions(className: 'Messages', javaOut: ''); + const generator = JavaGenerator(); + generator.generate(javaOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('public static final String stringConst = "hello";')); + expect(code, contains('public static final long intConst = 42L;')); + expect(code, contains('public static final double doubleConst = 3.14;')); + expect(code, contains('public static final boolean boolConst = true;')); + }); } diff --git a/packages/pigeon/test/kotlin_generator_test.dart b/packages/pigeon/test/kotlin_generator_test.dart index 5164c203860e..4daaadba5b79 100644 --- a/packages/pigeon/test/kotlin_generator_test.dart +++ b/packages/pigeon/test/kotlin_generator_test.dart @@ -1827,4 +1827,55 @@ void main() { expect(code, contains('override fun toString(): String {')); expect(code, contains(r'return "Foobar(field1=$field1, field2=$field2)"')); }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + Constant( + name: 'stringWithBackslashDollar', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: r'\$', + ), + Constant( + name: 'stringWithTwoBackslashesDollar', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: r'\\$', + ), + ], + ); + final sink = StringBuffer(); + const kotlinOptions = InternalKotlinOptions(kotlinOut: ''); + const generator = KotlinGenerator(); + generator.generate(kotlinOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('const val stringConst: String = "hello"')); + expect(code, contains('const val intConst: Long = 42L')); + expect(code, contains('const val doubleConst: Double = 3.14')); + expect(code, contains('const val boolConst: Boolean = true')); + expect(code, contains(r'const val stringWithBackslashDollar: String = "\\\$"')); + expect(code, contains(r'const val stringWithTwoBackslashesDollar: String = "\\\\\$"')); + }); } diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart index a5f8179fe84c..a93f3270b9d2 100644 --- a/packages/pigeon/test/objc_generator_test.dart +++ b/packages/pigeon/test/objc_generator_test.dart @@ -3548,4 +3548,98 @@ void main() { expect(code, contains('- (NSString *)description {')); expect(code, contains('return [NSString stringWithFormat:@"ABCFoo()"];')); }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const generator = ObjcGenerator(); + final generatorOptions = OutputFileOptions( + fileType: FileType.header, + languageOptions: const InternalObjcOptions( + prefix: 'ABC', + objcHeaderOut: '', + objcSourceOut: '', + headerIncludePath: '', + ), + ); + generator.generate(generatorOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('extern NSString *const ABCStringConst;')); + expect(code, contains('extern const NSInteger ABCIntConst;')); + expect(code, contains('extern const double ABCDoubleConst;')); + expect(code, contains('extern const BOOL ABCBoolConst;')); + }); + + test('gen constants source', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const generator = ObjcGenerator(); + final generatorOptions = OutputFileOptions( + fileType: FileType.source, + languageOptions: const InternalObjcOptions( + prefix: 'ABC', + objcHeaderOut: '', + objcSourceOut: '', + headerIncludePath: '', + ), + ); + generator.generate(generatorOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('NSString *const ABCStringConst = @"hello";')); + expect(code, contains('const NSInteger ABCIntConst = 42;')); + expect(code, contains('const double ABCDoubleConst = 3.14;')); + expect(code, contains('const BOOL ABCBoolConst = YES;')); + }); } diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 0273624ee61b..e8df30b2ec13 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -1888,4 +1888,118 @@ abstract class Api { await completer.future; }); }); + + group('constants parsing', () { + test('valid constants', () { + const code = ''' +const String myString = 'hello'; +const int myInt = 42; +const double myDouble = 3.14; +const bool myBool = true; +const int myNegativeInt = -10; +const double myNegativeDouble = -2.5; +const String myAdjacentString = 'hello ' 'world'; +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors, isEmpty); + expect(parseResult.root.constants.length, 7); + + final Constant myString = parseResult.root.constants[0]; + expect(myString.name, 'myString'); + expect(myString.type.baseName, 'String'); + expect(myString.value, 'hello'); + + final Constant myInt = parseResult.root.constants[1]; + expect(myInt.name, 'myInt'); + expect(myInt.type.baseName, 'int'); + expect(myInt.value, 42); + + final Constant myDouble = parseResult.root.constants[2]; + expect(myDouble.name, 'myDouble'); + expect(myDouble.type.baseName, 'double'); + expect(myDouble.value, 3.14); + + final Constant myBool = parseResult.root.constants[3]; + expect(myBool.name, 'myBool'); + expect(myBool.type.baseName, 'bool'); + expect(myBool.value, true); + + final Constant myNegativeInt = parseResult.root.constants[4]; + expect(myNegativeInt.name, 'myNegativeInt'); + expect(myNegativeInt.type.baseName, 'int'); + expect(myNegativeInt.value, -10); + + final Constant myNegativeDouble = parseResult.root.constants[5]; + expect(myNegativeDouble.name, 'myNegativeDouble'); + expect(myNegativeDouble.type.baseName, 'double'); + expect(myNegativeDouble.value, -2.5); + + final Constant myAdjacentString = parseResult.root.constants[6]; + expect(myAdjacentString.name, 'myAdjacentString'); + expect(myAdjacentString.type.baseName, 'String'); + expect(myAdjacentString.value, 'hello world'); + }); + + test('missing type annotation error', () { + const code = ''' +const myConst = 42; +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors, isNotEmpty); + expect( + parseResult.errors[0].message, + contains('Top-level constants must have an explicit type annotation.'), + ); + }); + + test('unsupported constant type error', () { + const code = ''' +const List myConst = 42; +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors, isNotEmpty); + expect( + parseResult.errors[0].message, + contains( + 'Unsupported constant type: "List". Only String, int, double, and bool are supported.', + ), + ); + }); + + test('constant value type mismatch error', () { + const code = ''' +const String myConst = 42; +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors, isNotEmpty); + expect( + parseResult.errors[0].message, + contains('Constant "myConst" type is String but value is int.'), + ); + }); + + test('unsupported expression type error', () { + const code = ''' +const int myConst = 1 + 2; +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors, isNotEmpty); + expect( + parseResult.errors[0].message, + contains('Unsupported expression type BinaryExpressionImpl for constant initializer.'), + ); + }); + + test('string interpolation error', () { + const code = r''' +const String myConst = 'hello ${1 + 2}'; +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors, isNotEmpty); + expect( + parseResult.errors[0].message, + contains('String interpolation is not supported in Pigeon constants.'), + ); + }); + }); } diff --git a/packages/pigeon/test/swift_generator_test.dart b/packages/pigeon/test/swift_generator_test.dart index 5c5f0417fe15..31032779c9d6 100644 --- a/packages/pigeon/test/swift_generator_test.dart +++ b/packages/pigeon/test/swift_generator_test.dart @@ -1537,4 +1537,43 @@ void main() { expect(code, contains('public var description: String {')); expect(code, contains(r'return "Foobar(field1: \(String(describing: field1)))"')); }); + + test('gen constants', () { + final root = Root( + apis: [], + classes: [], + enums: [], + constants: [ + Constant( + name: 'stringConst', + type: const TypeDeclaration(baseName: 'String', isNullable: false), + value: 'hello', + ), + Constant( + name: 'intConst', + type: const TypeDeclaration(baseName: 'int', isNullable: false), + value: 42, + ), + Constant( + name: 'doubleConst', + type: const TypeDeclaration(baseName: 'double', isNullable: false), + value: 3.14, + ), + Constant( + name: 'boolConst', + type: const TypeDeclaration(baseName: 'bool', isNullable: false), + value: true, + ), + ], + ); + final sink = StringBuffer(); + const swiftOptions = InternalSwiftOptions(swiftOut: ''); + const generator = SwiftGenerator(); + generator.generate(swiftOptions, root, sink, dartPackageName: DEFAULT_PACKAGE_NAME); + final code = sink.toString(); + expect(code, contains('let stringConst: String = "hello"')); + expect(code, contains('let intConst: Int64 = 42')); + expect(code, contains('let doubleConst: Double = 3.14')); + expect(code, contains('let boolConst: Bool = true')); + }); }