mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
3a6327a5d9
Summary: Open source this ESLint rule so that we can lint our open source NativeModule specs. Changelog: [Internal] Reviewed By: shergin, cpojer Differential Revision: D23791748 fbshipit-source-id: e44444bc87eaa9dc9b7f2b3ed03151798a35e8a5
468 lines
11 KiB
JavaScript
468 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react_native
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
|
|
const supportedTypes = [
|
|
'ArrayTypeAnnotation',
|
|
'BooleanTypeAnnotation',
|
|
'NumberTypeAnnotation',
|
|
'StringTypeAnnotation',
|
|
];
|
|
|
|
const supportedTypeAliases = {
|
|
BooleanTypeAnnotation: 'boolean',
|
|
FunctionTypeAnnotation: 'Function',
|
|
NumberTypeAnnotation: 'number',
|
|
StringTypeAnnotation: 'string',
|
|
ObjectTypeAnnotation: 'object',
|
|
};
|
|
|
|
const supportedGenericTypes = [
|
|
'Array',
|
|
'Object',
|
|
'RootTag',
|
|
'$ReadOnly',
|
|
'$ReadOnlyArray',
|
|
];
|
|
|
|
const supportedNullableTypes = [
|
|
'ArrayTypeAnnotation',
|
|
'BooleanTypeAnnotation',
|
|
'FunctionTypeAnnotation',
|
|
'NumberTypeAnnotation',
|
|
'ObjectTypeAnnotation',
|
|
'StringTypeAnnotation',
|
|
];
|
|
|
|
const supportedMethodReturnTypes = [
|
|
'BooleanTypeAnnotation',
|
|
'NumberTypeAnnotation',
|
|
'ObjectTypeAnnotation',
|
|
'StringTypeAnnotation',
|
|
'VoidTypeAnnotation',
|
|
];
|
|
|
|
const errors = {
|
|
invalidNativeModuleInterfaceName(interfaceName) {
|
|
return (
|
|
"NativeModule interfaces must be named 'Spec', " +
|
|
`got '${interfaceName}'.`
|
|
);
|
|
},
|
|
inexactObjectReturnType() {
|
|
return 'Spec interface method object return type must be exact.';
|
|
},
|
|
invalidHasteName(hasteName) {
|
|
return (
|
|
'Module name for a NativeModule JS wrapper must start with ' +
|
|
`'Native', got '${hasteName}' instead.`
|
|
);
|
|
},
|
|
missingSpecInterfaceMethod() {
|
|
return 'NativeModule Spec interface must define at least one method';
|
|
},
|
|
unsupportedMethodReturnType(typeName) {
|
|
return (
|
|
`Spec interface method has unsupported return type '${typeName}'. ` +
|
|
'See https://fburl.com/rn-nativemodules for more details.'
|
|
);
|
|
},
|
|
unsupportedType(typeName) {
|
|
return (
|
|
`Unsupported type '${typeName}' for Spec interface. ` +
|
|
'See https://fburl.com/rn-nativemodules for more details.'
|
|
);
|
|
},
|
|
untypedModuleRequire(requireMethodName) {
|
|
return (
|
|
'NativeModule require not type-safe. Please require with the NativeModule interface ' +
|
|
`'Spec': TurboModuleRegistry.${requireMethodName}<Spec>`
|
|
);
|
|
},
|
|
incorrectlyTypedModuleRequire(requireMethodName) {
|
|
return (
|
|
'NativeModule require incorrectly typed. Please require with the NativeModule interface identifier ' +
|
|
`'Spec', and nothing else: TurboModuleRegistry.${requireMethodName}<Spec>`
|
|
);
|
|
},
|
|
specNotDeclaredInFile() {
|
|
return "The NativeModule interface 'Spec' wasn't declared in this NativeModule spec file.";
|
|
},
|
|
};
|
|
|
|
function interfaceExtendsFrom(node, superInterfaceName) {
|
|
return (
|
|
node.type === 'InterfaceDeclaration' &&
|
|
node.extends[0] &&
|
|
node.extends[0].id.name === superInterfaceName
|
|
);
|
|
}
|
|
|
|
function isSupportedFunctionParam(node) {
|
|
if (node.type !== 'FunctionTypeParam') {
|
|
return false;
|
|
}
|
|
|
|
if (node.optional) {
|
|
return false;
|
|
}
|
|
|
|
return findUnsupportedType(node.typeAnnotation, true) == null;
|
|
}
|
|
|
|
function findUnsupportedType(typeAnnotation, supportCallbacks) {
|
|
if (supportedTypes.includes(typeAnnotation.type)) {
|
|
return null;
|
|
}
|
|
|
|
if (typeAnnotation.type === 'NullableTypeAnnotation') {
|
|
if (supportedNullableTypes.includes(typeAnnotation.typeAnnotation.type)) {
|
|
return null;
|
|
}
|
|
typeAnnotation = typeAnnotation.typeAnnotation;
|
|
}
|
|
|
|
if (typeAnnotation.type === 'FunctionTypeAnnotation' && supportCallbacks) {
|
|
return null;
|
|
}
|
|
|
|
if (typeAnnotation.type === 'GenericTypeAnnotation') {
|
|
if (!supportedGenericTypes.includes(typeAnnotation.id.name)) {
|
|
return typeAnnotation;
|
|
}
|
|
if (
|
|
!isGenericArrayTypeAnnotation(typeAnnotation) &&
|
|
typeAnnotation.typeParameters
|
|
) {
|
|
for (const param of typeAnnotation.typeParameters.params) {
|
|
const unsupported = findUnsupportedType(param, supportCallbacks);
|
|
if (unsupported != null) {
|
|
return unsupported;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (typeAnnotation.type === 'ObjectTypeAnnotation') {
|
|
for (const prop of typeAnnotation.properties) {
|
|
const unsupported = findUnsupportedType(prop.value, supportCallbacks);
|
|
if (unsupported != null) {
|
|
return unsupported;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return typeAnnotation;
|
|
}
|
|
|
|
function functionParamTypeName(node) {
|
|
if (node.type !== 'FunctionTypeParam') {
|
|
return null;
|
|
}
|
|
|
|
const parts = [];
|
|
if (node.optional) {
|
|
parts.push('optional');
|
|
}
|
|
parts.push(functionParamTypeAnnotationName(node.typeAnnotation));
|
|
return parts.join(' ');
|
|
}
|
|
|
|
function functionParamTypeAnnotationName(typeAnnotation) {
|
|
const {id, type} = typeAnnotation;
|
|
if (type === 'GenericTypeAnnotation') {
|
|
return id.name;
|
|
}
|
|
|
|
const parts = [];
|
|
if (type === 'NullableTypeAnnotation') {
|
|
parts.push('nullable');
|
|
parts.push(functionParamTypeAnnotationName(typeAnnotation.typeAnnotation));
|
|
} else {
|
|
parts.push(supportedTypeAliases[type] || type);
|
|
}
|
|
return parts.join(' ');
|
|
}
|
|
|
|
function getTypeName(typeAnnotation) {
|
|
const {id, type} = typeAnnotation;
|
|
if (type === 'GenericTypeAnnotation') {
|
|
return id.name;
|
|
}
|
|
return supportedTypeAliases[type];
|
|
}
|
|
|
|
function checkSupportedSpecProperty(context, node) {
|
|
if (node.type !== 'FunctionTypeAnnotation') {
|
|
const unsupportedNode = findUnsupportedType(node, false);
|
|
if (unsupportedNode != null) {
|
|
context.report({
|
|
node: unsupportedNode,
|
|
message: errors.unsupportedType(
|
|
functionParamTypeAnnotationName(unsupportedNode),
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!isSupportedMethodReturnTypeAnnotation(node.returnType)) {
|
|
context.report({
|
|
node: node.returnType,
|
|
message: errors.unsupportedMethodReturnType(getTypeName(node.returnType)),
|
|
});
|
|
return false;
|
|
}
|
|
|
|
// Check for exact object return type.
|
|
if (
|
|
node.returnType.type === 'ObjectTypeAnnotation' &&
|
|
!node.returnType.exact
|
|
) {
|
|
context.report({
|
|
node: node.returnType,
|
|
message: errors.inexactObjectReturnType(),
|
|
});
|
|
}
|
|
|
|
for (const param of node.params) {
|
|
if (!isSupportedFunctionParam(param)) {
|
|
context.report({
|
|
node: param.typeAnnotation,
|
|
message: errors.unsupportedType(functionParamTypeName(param)),
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function isPromiseTypeAnnotation(typeAnnotation) {
|
|
return (
|
|
typeAnnotation.type === 'GenericTypeAnnotation' &&
|
|
typeAnnotation.id &&
|
|
typeAnnotation.id.name === 'Promise'
|
|
);
|
|
}
|
|
|
|
function isGenericArrayTypeAnnotation(typeAnnotation) {
|
|
return (
|
|
typeAnnotation.type === 'GenericTypeAnnotation' &&
|
|
typeAnnotation.id &&
|
|
typeAnnotation.id.name === 'Array'
|
|
);
|
|
}
|
|
|
|
function isGenericObjectTypeAnnotation(typeAnnotation) {
|
|
return (
|
|
typeAnnotation.type === 'GenericTypeAnnotation' &&
|
|
typeAnnotation.id &&
|
|
typeAnnotation.id.name === 'Object'
|
|
);
|
|
}
|
|
|
|
function isSupportedMethodReturnTypeAnnotation(typeAnnotation) {
|
|
const resolvedType =
|
|
typeAnnotation.type === 'NullableTypeAnnotation'
|
|
? typeAnnotation.typeAnnotation
|
|
: typeAnnotation;
|
|
return (
|
|
supportedMethodReturnTypes.includes(resolvedType.type) ||
|
|
isGenericArrayTypeAnnotation(resolvedType) ||
|
|
isGenericObjectTypeAnnotation(resolvedType) ||
|
|
isPromiseTypeAnnotation(resolvedType)
|
|
);
|
|
}
|
|
|
|
const VALID_SPEC_NAMES = /^Native\S+$/;
|
|
|
|
function isModuleRequire(node) {
|
|
if (node.type !== 'CallExpression') {
|
|
return false;
|
|
}
|
|
|
|
const callExpression = node;
|
|
|
|
if (callExpression.callee.type !== 'MemberExpression') {
|
|
return false;
|
|
}
|
|
|
|
const memberExpression = callExpression.callee;
|
|
if (
|
|
!(
|
|
memberExpression.object.type === 'Identifier' &&
|
|
memberExpression.object.name === 'TurboModuleRegistry'
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
!(
|
|
memberExpression.property.type === 'Identifier' &&
|
|
(memberExpression.property.name === 'get' ||
|
|
memberExpression.property.name === 'getEnforcing')
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isGeneratedFile(context) {
|
|
return (
|
|
context
|
|
.getSourceCode()
|
|
.getText()
|
|
.indexOf('@' + 'generated SignedSource<<') !== -1
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A lint rule to guide best practices in writing type safe React NativeModules.
|
|
*/
|
|
function rule(context) {
|
|
const filename = context.getFilename();
|
|
|
|
if (isGeneratedFile(context)) {
|
|
return {};
|
|
}
|
|
|
|
const sourceCode = context.getSourceCode().getText();
|
|
if (!sourceCode.includes('TurboModuleRegistry')) {
|
|
return {};
|
|
}
|
|
|
|
const specIdentifierUsages = [];
|
|
const declaredModuleInterfaces = [];
|
|
|
|
return {
|
|
'Program:exit': function() {
|
|
if (
|
|
specIdentifierUsages.length > 0 &&
|
|
declaredModuleInterfaces.length === 0
|
|
) {
|
|
specIdentifierUsages.forEach(specNode => {
|
|
context.report({
|
|
node: specNode,
|
|
message: errors.specNotDeclaredInFile(),
|
|
});
|
|
});
|
|
}
|
|
},
|
|
CallExpression(node) {
|
|
if (!isModuleRequire(node)) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Validate that NativeModule requires are typed
|
|
*/
|
|
|
|
const {typeArguments} = node;
|
|
|
|
if (typeArguments == null) {
|
|
const methodName = node.callee.property.name;
|
|
context.report({
|
|
node,
|
|
message: errors.untypedModuleRequire(methodName),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (typeArguments.type !== 'TypeParameterInstantiation') {
|
|
return;
|
|
}
|
|
|
|
const [param] = typeArguments.params;
|
|
|
|
/**
|
|
* Validate that NativeModule requires are correctly typed
|
|
*/
|
|
|
|
if (
|
|
typeArguments.params.length !== 1 ||
|
|
param.type !== 'GenericTypeAnnotation' ||
|
|
param.id.name !== 'Spec'
|
|
) {
|
|
const methodName = node.callee.property.name;
|
|
context.report({
|
|
node,
|
|
message: errors.incorrectlyTypedModuleRequire(methodName),
|
|
});
|
|
return;
|
|
}
|
|
|
|
specIdentifierUsages.push(param);
|
|
return true;
|
|
},
|
|
InterfaceDeclaration(node) {
|
|
if (
|
|
!interfaceExtendsFrom(node, 'DEPRECATED_RCTExport') &&
|
|
!interfaceExtendsFrom(node, 'TurboModule')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const basename = path.basename(filename, '.js');
|
|
if (
|
|
basename &&
|
|
basename !== 'RCTExport' &&
|
|
!VALID_SPEC_NAMES.test(basename)
|
|
) {
|
|
context.report({
|
|
loc: {start: {line: 0, column: 0}},
|
|
message: errors.invalidHasteName(basename),
|
|
});
|
|
}
|
|
|
|
if (node.id.name !== 'Spec') {
|
|
context.report({
|
|
node,
|
|
message: errors.invalidNativeModuleInterfaceName(node.id.name),
|
|
fix: fixer => fixer.replaceText(node.id, 'Spec'),
|
|
});
|
|
return;
|
|
}
|
|
|
|
declaredModuleInterfaces.push(node);
|
|
|
|
if (!node.body.properties.length) {
|
|
context.report({
|
|
node: node.body,
|
|
message: errors.missingSpecInterfaceMethod(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
let hasUnsupportedProp = false;
|
|
node.body.properties.forEach(prop => {
|
|
if (hasUnsupportedProp) {
|
|
return;
|
|
}
|
|
if (!checkSupportedSpecProperty(context, prop.value)) {
|
|
hasUnsupportedProp = true;
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
rule.errors = errors;
|
|
|
|
module.exports = rule;
|