Files
lexical/scripts/lint-flow-types.js

175 lines
4.9 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.
*
*/
// @ts-check
'use strict';
const fs = require('fs-extra');
const ts = require('typescript');
const tsMorph = require('ts-morph');
const hermesParser = require('hermes-parser');
const {packagesManager} = require('./shared/packagesManager');
const pretty = process.env.CI !== 'true';
/** @type {ts.FormatDiagnosticsHost} */
const diagnosticsHost = {
getCanonicalFileName: (fn) => fn,
getCurrentDirectory: () => './',
getNewLine: () => '\n',
};
/**
* Validate that the manually maintained .flow types have the same exports as
* the corresponding .d.ts types produced by the build.
*
* `process.exit(1)` on failure.
*/
function lintFlowTypes() {
let didError = false;
const project = new tsMorph.Project({tsConfigFilePath: './tsconfig.json'});
for (const pkg of packagesManager.getPublicPackages()) {
didError = lintFlowTypesForPackage(project, pkg) || didError;
}
if (didError) {
process.exit(1);
}
}
function collectFlowExports(flowAst) {
const exportNames = new Map();
const exportId = (node) => {
const identifier =
node.type === 'Identifier'
? node
: 'id' in node && node.id.type === 'Identifier'
? node.id
: null;
if (identifier) {
exportNames.set(identifier.name, identifier);
return true;
}
return false;
};
hermesParser.SimpleTraverser.traverse(flowAst, {
enter: (node, parent) => {
if (
parent &&
(parent.type === 'DeclareExportDeclaration' ||
parent.type === 'ExportNamedDeclaration')
) {
if (exportId(node)) {
// ok
} else if (node.type === 'VariableDeclaration') {
for (const declaration of node.declarations) {
if (!exportId(declaration)) {
// debugger;
}
}
} else if (node.type === 'ExportSpecifier') {
if (!exportId(node.exported)) {
// debugger;
}
} else {
// debugger;
}
}
},
leave: () => {},
});
return exportNames;
}
function compareFlowDts(
/** @type {PackageMetadata} */ pkg,
/** @type {string} */ flowFilePath,
/** @type {tsMorph.SourceFile} */ entrypoint,
/** @type {ts.Diagnostic[]} */ diagnostics,
/** @type {import('hermes-estree').Identifier[]} */ flowDiagnostics,
) {
const flowAst = hermesParser.parse(fs.readFileSync(flowFilePath, 'utf-8'), {
enableExperimentalComponentSyntax: true,
flow: 'all',
sourceFilename: flowFilePath,
sourceType: 'module',
});
const flowMap = collectFlowExports(flowAst);
const symbols = entrypoint.getExportSymbols();
const tsMap = new Map(symbols.map((sym) => [sym.getName(), sym]));
for (const [name, symbol] of tsMap) {
if (flowMap.has(name)) {
continue;
}
for (const decl of symbol.getDeclarations()) {
const start = decl.getStart();
const end = decl.getEnd();
diagnostics.push({
category: ts.DiagnosticCategory.Warning,
code: Infinity,
file: entrypoint.compilerNode,
length: end - start,
messageText: `Missing flow export for TypeScript export '${name}'`,
start,
});
break;
}
// debugger;
}
for (const [name, flowToken] of flowMap) {
if (tsMap.has(name)) {
continue;
}
flowDiagnostics.push(flowToken);
}
}
function lintFlowTypesForPackage(
/** @type {tsMorph.Project} */ project,
/** @type {PackageMetadata} */ pkg,
) {
const def = pkg.getPackageBuildDefinition();
if (def.packageName === 'lexical-eslint-plugin') {
return false;
}
/** @type {ts.Diagnostic[]} */
const diagnostics = [];
/** @type {import('hermes-estree').Identifier[]} */
const flowDiagnostics = [];
for (const {outputFileName, sourceFileName} of def.modules) {
const entrypoint = project.addSourceFileAtPath(
pkg.resolve('src', sourceFileName),
);
const flowFilePath = pkg.resolve('flow', `${outputFileName}.js.flow`);
if (!fs.existsSync(flowFilePath)) {
console.error(`Missing ${flowFilePath}`);
process.exit(1);
}
compareFlowDts(pkg, flowFilePath, entrypoint, diagnostics, flowDiagnostics);
}
if (diagnostics.length > 0 || flowDiagnostics.length > 0) {
const msg = (
pretty ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics
)(diagnostics, diagnosticsHost);
if (msg) {
console.error(msg.replace(/ TSInfinity:/g, ':'));
}
const flowMsg = flowDiagnostics
.map(
(ident) =>
`${ident.loc.source}:${ident.loc.start.line}:${ident.loc.start.column} - warning: Flow export '${ident.name}' does not have a TypeScript declaration`,
)
.join('\n');
if (flowMsg) {
console.error(flowMsg);
}
return true;
}
return false;
}
lintFlowTypes();