blob: 945b4ee4d205d773534f20ab147f64f80efa5666 [file] [log] [blame]
// 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);
});
}