| // 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', | |
| ); | |
| // 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(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', 'web_ui', 'lib', '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('AppLifecycleState enums match', () { | |
| // Dart values: _kFooBarIndex = 1 << N. | |
| final List<String> uiFields = getDartClassFields( | |
| sourcePath: path.join(flutterRoot, 'lib', 'ui', 'platform_dispatcher.dart'), | |
| className: 'AppLifecycleState', | |
| ); | |
| final List<String> webuiFields = getDartClassFields( | |
| sourcePath: path.join(flutterRoot, 'lib', 'web_ui', 'lib', 'platform_dispatcher.dart'), | |
| className: 'AppLifecycleState', | |
| ); | |
| // C++ values: kFooBar = 1 << N. | |
| final List<String> internalEnumValues = getCppEnumClassValues( | |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'common', 'app_lifecycle_state.h'), | |
| enumName: 'AppLifecycleState', | |
| ); | |
| // Java values: FOO_BAR(1 << N). | |
| final List<String> javaEnumValues = getJavaEnumValues( | |
| sourcePath: path.join(flutterRoot, 'shell', 'platform', 'android', 'io', | |
| 'flutter', 'embedding', 'engine', 'systemchannels', 'LifecycleChannel.java'), | |
| enumName: 'AppLifecycleState', | |
| ).map(allCapsToCamelCase).toList(); | |
| expect(webuiFields, 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).name2.lexeme; | |
| 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); | |
| }); | |
| } |