react-native/packages/eslint-plugin-specs/react-native-modules.js
Moti Zilberman a74765eb0a Throw Flow syntax errors instead of continuing to process the AST (#39035)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/39035

Changelog: [General][Fixed] Flow syntax errors in Codegen specs are no longer ignored.

Instead of throwing errors like most parsers, the `flow-parser` package returns errors as part of the AST (along with a best-effort parse). It turns out that `react-native/codegen` ignores such errors and only detects a subset of them after the fact. Here we change the behaviour to immediately throwing a descriptive error message (containing the file name and a code frame).

**This change is theoretically breaking** for any published packages that already contain broken Flow code (that somehow doesn't happen to affect the Codegen output today). Hopefully, anyone using Flow-flavoured RN Codegen is also typechecking with Flow and/or building with Metro (which would both flag the same errors), so the impact should be fairly contained.

Reviewed By: huntie

Differential Revision: D48385786

fbshipit-source-id: c7e1f5fb64a61fb0eb9e9f8f7501b43264c9626c
2023-11-08 01:33:42 -08:00

198 lines
5.0 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/
'use strict';
const withBabelRegister = require('./with-babel-register');
const path = require('path');
// We use the prepack hook before publishing package to set this value to true
const PACKAGE_USAGE = false;
const ERRORS = {
misnamedHasteModule(hasteModuleName) {
return `Module ${hasteModuleName}: All files using TurboModuleRegistry must start with Native.`;
},
};
let RNModuleParser;
let RNParserUtils;
let RNFlowParser;
let RNParserCommons;
let RNFlowParserUtils;
function requireModuleParser() {
if (
RNModuleParser == null ||
RNParserUtils == null ||
RNFlowParser == null ||
RNParserCommons == null ||
RNFlowParserUtils == null
) {
// If using this externally, we leverage @react-native/codegen as published form
if (!PACKAGE_USAGE) {
const config = {
only: [/react-native-codegen\/src\//],
plugins: [require('@babel/plugin-transform-flow-strip-types').default],
};
withBabelRegister(config, () => {
RNModuleParser = require('@react-native/codegen/src/parsers/flow/modules');
RNParserUtils = require('@react-native/codegen/src/parsers/utils');
RNFlowParser = require('@react-native/codegen/src/parsers/flow/parser');
RNParserCommons = require('@react-native/codegen/src/parsers/parsers-commons');
RNFlowParserUtils = require('@react-native/codegen/src/parsers/flow/utils');
});
} else {
const config = {
only: [/@react-native\/codegen\/lib\//],
plugins: [require('@babel/plugin-transform-flow-strip-types').default],
};
withBabelRegister(config, () => {
RNModuleParser = require('@react-native/codegen/lib/parsers/flow/modules');
RNParserUtils = require('@react-native/codegen/lib/parsers/utils');
RNFlowParser = require('@react-native/codegen/lib/parsers/flow/parser');
RNParserCommons = require('@react-native/codegen/lib/parsers/parsers-commons');
RNFlowParserUtils = require('@react-native/codegen/lib/parsers/flow/utils');
});
}
}
return {
buildModuleSchema: RNParserCommons.buildModuleSchema,
createParserErrorCapturer: RNParserUtils.createParserErrorCapturer,
parser: new RNFlowParser.FlowParser(),
translateTypeAnnotation: RNModuleParser.flowTranslateTypeAnnotation,
};
}
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();
const hasteModuleName = path.basename(filename).replace(/\.js$/, '');
if (isGeneratedFile(context)) {
return {};
}
let isModule = false;
return {
'Program:exit': function (node) {
if (!isModule) {
return;
}
// Report invalid file names
if (!VALID_SPEC_NAMES.test(hasteModuleName)) {
context.report({
node,
message: ERRORS.misnamedHasteModule(hasteModuleName),
});
}
const {
buildModuleSchema,
createParserErrorCapturer,
parser,
translateTypeAnnotation,
} = requireModuleParser();
const [parsingErrors, tryParse] = createParserErrorCapturer();
const sourceCode = context.getSourceCode().getText();
const ast = parser.getAst(sourceCode, filename);
tryParse(() => {
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
parser,
translateTypeAnnotation,
);
});
parsingErrors.forEach(error => {
error.nodes.forEach(flowNode => {
context.report({
loc: flowNode.loc,
message: error.message,
});
});
});
},
CallExpression(node) {
if (!isModuleRequire(node)) {
return;
}
isModule = true;
},
InterfaceExtends(node) {
if (node.id.name !== 'TurboModule') {
return;
}
isModule = true;
},
};
}
rule.errors = ERRORS;
module.exports = rule;