mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 06:59:17 +08:00
[*] Feature: Add linter to check that flow types are consistent with typescript types (#7230)
This commit is contained in:
100
package-lock.json
generated
100
package-lock.json
generated
@ -69,6 +69,7 @@
|
||||
"glob": "^10.4.1",
|
||||
"google-closure-compiler": "^20220202.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"hermes-estree": "^0.26.0",
|
||||
"hermes-parser": "^0.26.0",
|
||||
"hermes-transform": "^0.26.0",
|
||||
"husky": "^7.0.1",
|
||||
@ -87,6 +88,7 @@
|
||||
"rollup": "^4.22.4",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-morph": "^25.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.25.12",
|
||||
"typescript": "^5.4.5",
|
||||
@ -8824,6 +8826,41 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.26.1.tgz",
|
||||
"integrity": "sha512-Sn28TGl/4cFpcM+jwsH1wLncYq3FtN/BIpem+HOygfBWPT5pAeS5dB4VFVzV8FbnOKHpDLZmvAl4AjPEev5idA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@ -12109,6 +12146,12 @@
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/code-block-writer": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/collapse-white-space": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
|
||||
@ -34545,6 +34588,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-morph": {
|
||||
"version": "25.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz",
|
||||
"integrity": "sha512-QJEiTdnz1YjrB3JFhd626gX4rKHDLSjSVMvGGG4v7ONc3RBwa0Eei98G9AT9uNFDMtV54JyuXsFeC+OH0n6bXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ts-morph/common": "~0.26.0",
|
||||
"code-block-writer": "^13.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
@ -44261,6 +44314,37 @@
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
|
||||
},
|
||||
"@ts-morph/common": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.26.1.tgz",
|
||||
"integrity": "sha512-Sn28TGl/4cFpcM+jwsH1wLncYq3FtN/BIpem+HOygfBWPT5pAeS5dB4VFVzV8FbnOKHpDLZmvAl4AjPEev5idA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-glob": "^3.3.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@ -46800,6 +46884,12 @@
|
||||
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
|
||||
"dev": true
|
||||
},
|
||||
"code-block-writer": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
|
||||
"dev": true
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
|
||||
@ -62090,6 +62180,16 @@
|
||||
"yargs-parser": "^21.0.1"
|
||||
}
|
||||
},
|
||||
"ts-morph": {
|
||||
"version": "25.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz",
|
||||
"integrity": "sha512-QJEiTdnz1YjrB3JFhd626gX4rKHDLSjSVMvGGG4v7ONc3RBwa0Eei98G9AT9uNFDMtV54JyuXsFeC+OH0n6bXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ts-morph/common": "~0.26.0",
|
||||
"code-block-writer": "^13.0.3"
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
|
@ -25,6 +25,7 @@
|
||||
"build-release": "npm run build-prod -- --release --codes",
|
||||
"build-www": "npm run clean && npm run build -- --www && npm run build -- --www --prod && npm run prepare-www",
|
||||
"build-types": "tsc -p ./tsconfig.build.json && node ./scripts/validate-tsc-types.js",
|
||||
"lint-flow": "node ./scripts/lint-flow-types.js",
|
||||
"clean": "node scripts/clean.js",
|
||||
"extract-codes": "node scripts/build.js --codes",
|
||||
"flow": "node ./scripts/check-flow-types.js",
|
||||
@ -163,6 +164,7 @@
|
||||
"glob": "^10.4.1",
|
||||
"google-closure-compiler": "^20220202.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"hermes-estree": "^0.26.0",
|
||||
"hermes-parser": "^0.26.0",
|
||||
"hermes-transform": "^0.26.0",
|
||||
"husky": "^7.0.1",
|
||||
@ -181,6 +183,7 @@
|
||||
"rollup": "^4.22.4",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-morph": "^25.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.25.12",
|
||||
"typescript": "^5.4.5",
|
||||
|
174
scripts/lint-flow-types.js
Normal file
174
scripts/lint-flow-types.js
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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();
|
Reference in New Issue
Block a user