| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:io'; |
| |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:apicheck/apicheck.dart'; |
| import 'package:litetest/litetest.dart'; |
| import 'package:path/path.dart' as path; |
| |
| void main(List<String> arguments) { |
| if (arguments.isEmpty) { |
| print('usage: dart bin/apicheck.dart path/to/engine/src/flutter'); |
| exit(1); |
| } |
| |
| final String flutterRoot = arguments[0]; |
| |
| checkApiConsistency(flutterRoot); |
| checkNativeApi(flutterRoot); |
| } |
| |
| /// Verify that duplicate Flutter API is consistent between implementations. |
| /// |
| /// Flutter contains API that is required to match between implementations. |
| /// Notably, some enums such as those used by Flutter accessibility, appear in: |
| /// * dart:ui (native) |
| /// * dart:ui (web) |
| /// * embedder.h |
| /// * Internal enumerations used by iOS/Android |
| /// |
| /// WARNING: The embedder API makes strong API/ABI stability guarantees. Care |
| /// must be taken when adding new enums, or values to existing enums. These |
| /// CANNOT be removed without breaking backward compatibility, which we have |
| /// never done. See the note at the top of `shell/platform/embedder/embedder.h` |
| /// for further details. |
| void checkApiConsistency(String flutterRoot) { |
| test('AccessibilityFeatures enums match', () { |
| // Dart values: _kFooBarIndex = 1 << N |
| final List<String> uiFields = getDartClassFields( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'window.dart'), |
| className: 'AccessibilityFeatures', |
| ); |
| final List<String> webuiFields = getDartClassFields( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'window.dart'), |
| className: 'AccessibilityFeatures', |
| ); |
| // C values: kFlutterAccessibilityFeatureFooBar = 1 << N, |
| final List<String> embedderEnumValues = getCppEnumValues( |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'embedder', 'embedder.h'), |
| enumName: 'FlutterAccessibilityFeature', |
| ); |
| // C++ values: kFooBar = 1 << N, |
| final List<String> internalEnumValues = getCppEnumClassValues( |
| sourcePath: path.join(flutterRoot, 'lib','ui', 'window', 'platform_configuration.h'), |
| enumName: 'AccessibilityFeatureFlag', |
| ); |
| // Java values: FOO_BAR(1 << N). |
| final List<String> javaEnumValues = getJavaEnumValues( |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'android', 'io', |
| 'flutter', 'view', 'AccessibilityBridge.java'), |
| enumName: 'AccessibilityFeature', |
| ).map(allCapsToCamelCase).toList(); |
| |
| expect(webuiFields, uiFields); |
| expect(embedderEnumValues, uiFields); |
| expect(internalEnumValues, uiFields); |
| expect(javaEnumValues, uiFields); |
| }); |
| |
| test('SemanticsAction enums match', () { |
| // Dart values: _kFooBarIndex = 1 << N. |
| final List<String> uiFields = getDartClassFields( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics.dart'), |
| className: 'SemanticsAction', |
| ); |
| final List<String> webuiFields = getDartClassFields( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics.dart'), |
| className: 'SemanticsAction', |
| ); |
| // C values: kFlutterSemanticsActionFooBar = 1 << N. |
| final List<String> embedderEnumValues = getCppEnumValues( |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'embedder', 'embedder.h'), |
| enumName: 'FlutterSemanticsAction', |
| ); |
| // C++ values: kFooBar = 1 << N. |
| final List<String> internalEnumValues = getCppEnumClassValues( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics', 'semantics_node.h'), |
| enumName: 'SemanticsAction', |
| ); |
| // Java values: FOO_BAR(1 << N). |
| final List<String> javaEnumValues = getJavaEnumValues( |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'android', 'io', |
| 'flutter', 'view', 'AccessibilityBridge.java'), |
| enumName: 'Action', |
| ).map(allCapsToCamelCase).toList(); |
| |
| expect(webuiFields, uiFields); |
| expect(embedderEnumValues, uiFields); |
| expect(internalEnumValues, uiFields); |
| expect(javaEnumValues, uiFields); |
| }); |
| |
| test('SemanticsFlag enums match', () { |
| // Dart values: _kFooBarIndex = 1 << N. |
| final List<String> uiFields = getDartClassFields( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics.dart'), |
| className: 'SemanticsFlag', |
| ); |
| final List<String> webuiFields = getDartClassFields( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics.dart'), |
| className: 'SemanticsFlag', |
| ); |
| // C values: kFlutterSemanticsFlagFooBar = 1 << N. |
| final List<String> embedderEnumValues = getCppEnumValues( |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'embedder', 'embedder.h'), |
| enumName: 'FlutterSemanticsFlag', |
| ); |
| // C++ values: kFooBar = 1 << N. |
| final List<String> internalEnumValues = getCppEnumClassValues( |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'semantics', 'semantics_node.h'), |
| enumName: 'SemanticsFlags', |
| ); |
| // Java values: FOO_BAR(1 << N). |
| final List<String> javaEnumValues = getJavaEnumValues( |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'android', 'io', |
| 'flutter', 'view', 'AccessibilityBridge.java'), |
| enumName: 'Flag', |
| ).map(allCapsToCamelCase).toList(); |
| |
| expect(webuiFields, uiFields); |
| expect(embedderEnumValues, uiFields); |
| expect(internalEnumValues, uiFields); |
| expect(javaEnumValues, uiFields); |
| }); |
| } |
| |
| /// Returns the CamelCase equivalent of an ALL_CAPS identifier. |
| String allCapsToCamelCase(String identifier) { |
| final StringBuffer buffer = StringBuffer(); |
| for (final String word in identifier.split('_')) { |
| if (word.isNotEmpty) { |
| buffer.write(word[0]); |
| } |
| if (word.length > 1) { |
| buffer.write(word.substring(1).toLowerCase()); |
| } |
| } |
| return buffer.toString(); |
| } |
| |
| /// Verify that the native functions in the dart:ui package do not use nullable |
| /// parameters that map to simple types such as numbers and strings. |
| /// |
| /// The Tonic argument converters used by the native function implementations |
| /// expect that values of these types will not be null. |
| class NativeFunctionVisitor extends RecursiveAstVisitor<void> { |
| final Set<String> simpleTypes = <String>{'int', 'double', 'bool', 'String'}; |
| |
| List<String> errors = <String>[]; |
| |
| @override |
| void visitNativeFunctionBody(NativeFunctionBody node) { |
| final MethodDeclaration? method = node.thisOrAncestorOfType<MethodDeclaration>(); |
| if (method != null) { |
| if (method.parameters != null) { |
| check(method.toString(), method.parameters!); |
| } |
| return; |
| } |
| |
| final FunctionDeclaration? func = node.thisOrAncestorOfType<FunctionDeclaration>(); |
| if (func != null) { |
| final FunctionExpression funcExpr = func.functionExpression; |
| if (funcExpr.parameters != null) { |
| check(func.toString(), funcExpr.parameters!); |
| } |
| return; |
| } |
| |
| throw Exception('unreachable'); |
| } |
| |
| void check(String description, FormalParameterList parameters) { |
| for (final FormalParameter parameter in parameters.parameters) { |
| TypeAnnotation? type; |
| if (parameter is SimpleFormalParameter) { |
| type = parameter.type; |
| } else if (parameter is DefaultFormalParameter) { |
| type = (parameter.parameter as SimpleFormalParameter).type; |
| } |
| if (type! is NamedType) { |
| final String name = (type as NamedType).name.name; |
| if (type.question != null && simpleTypes.contains(name)) { |
| errors.add(description); |
| return; |
| } |
| } |
| } |
| } |
| } |
| |
| void checkNativeApi(String flutterRoot) { |
| test('Native API does not pass nullable parameters of simple types', () { |
| final NativeFunctionVisitor visitor = NativeFunctionVisitor(); |
| visitUIUnits(flutterRoot, visitor); |
| expect(visitor.errors, isEmpty); |
| }); |
| } |