mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 00:20:28 +01:00
This PR updates to use SSA form through the entire compilation pipeline. This means that in both HIR form and ReactiveFunction form, `Identifier` instances map 1:1 to `IdentifierId` values. If two identifiers have the same IdentifierId, they are the same instance. What this means is that all our passes can use this more precise information to determine if two particular identifiers are not just the same variable, but the same SSA "version" of that variable. However, some parts of our analysis really care about program variables as opposed to SSA versions, and were relying on LeaveSSA to reset identifiers such that all Identifier instances for a particular program variable would have the same IdentifierId (though not necessarily the same Identifier instance). With LeaveSSA removed, those analysis passes can now use DeclarationId instead to uniquely identify a program variable. Note that this PR surfaces some opportunties to improve edge-cases around reassigned values being declared/reassigned/depended-upon across multiple scopes. Several passes could/should use IdentifierId to more precisely identify exactly which values are accessed - for example, a scope that reassigns `x` but doesn't use `x` prior to reassignment doesn't have to take a dependency on `x`. But today we take a dependnecy. My approach for these cases was to add a "TODO LeaveSSA" comment with notes and the name of the fixture demonstrating the difference, but to intentionally preserve the existing behavior (generally, switching to use DeclarationId when IdentifierId would have been more precise). Beyond updating passes to use DeclarationId instead of Identifier/IdentifierId, the other change here is to extract out the remaining necessary bits of LeaveSSA into a new pass that rewrites InstructionKind (const/let/reassign/etc) based on whether a value is actually const or has reassignments and should be let. ghstack-source-id: 69afdaee5fadf3fdc98ce97549da805f288218b4 Pull Request resolved: https://github.com/facebook/react/pull/30573
251 lines
5.8 KiB
JavaScript
251 lines
5.8 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.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const HermesParser = require('hermes-parser');
|
|
const BabelParser = require('@babel/parser');
|
|
const BabelCore = require('@babel/core');
|
|
const invariant = require('invariant');
|
|
const {argv, stdin} = require('process');
|
|
const prettier = require('prettier');
|
|
const {JSXText} = require('hermes-parser/dist/generated/ESTreeVisitorKeys');
|
|
|
|
function runPlugin(text, file, language) {
|
|
let ast;
|
|
if (language === 'flow') {
|
|
ast = HermesParser.parse(text, {
|
|
babel: true,
|
|
flow: 'all',
|
|
sourceFilename: file,
|
|
sourceType: 'module',
|
|
enableExperimentalComponentSyntax: true,
|
|
});
|
|
} else {
|
|
ast = BabelParser.parse(text, {
|
|
sourceFilename: file,
|
|
plugins: ['typescript', 'jsx'],
|
|
sourceType: 'module',
|
|
});
|
|
}
|
|
const result = BabelCore.transformFromAstSync(ast, text, {
|
|
ast: false,
|
|
filename: file,
|
|
highlightCode: false,
|
|
retainLines: true,
|
|
plugins: [[AnonymizePlugin]],
|
|
sourceType: 'module',
|
|
configFile: false,
|
|
babelrc: false,
|
|
});
|
|
invariant(
|
|
result?.code != null,
|
|
`Expected BabelPluginReactForget to codegen successfully, got: ${result}`
|
|
);
|
|
return result.code;
|
|
}
|
|
|
|
async function format(code, language) {
|
|
return await prettier.format(code, {
|
|
semi: true,
|
|
parser: language === 'typescript' ? 'babel-ts' : 'flow',
|
|
});
|
|
}
|
|
|
|
const TAG_NAMES = new Set([
|
|
'a',
|
|
'body',
|
|
'button',
|
|
'div',
|
|
'form',
|
|
'head',
|
|
'html',
|
|
'input',
|
|
'label',
|
|
'select',
|
|
'span',
|
|
'textarea',
|
|
|
|
// property/attribute names
|
|
'value',
|
|
'checked',
|
|
'onClick',
|
|
'onSubmit',
|
|
'name',
|
|
]);
|
|
|
|
const BUILTIN_HOOKS = new Set([
|
|
'useContext',
|
|
'useEffect',
|
|
'useInsertionEffect',
|
|
'useLayoutEffect',
|
|
'useReducer',
|
|
'useState',
|
|
]);
|
|
|
|
const GLOBALS = new Set([
|
|
'String',
|
|
'Object',
|
|
'Function',
|
|
'Number',
|
|
'RegExp',
|
|
'Date',
|
|
'Error',
|
|
'Function',
|
|
'TypeError',
|
|
'RangeError',
|
|
'ReferenceError',
|
|
'SyntaxError',
|
|
'URIError',
|
|
'EvalError',
|
|
'Boolean',
|
|
'DataView',
|
|
'Float32Array',
|
|
'Float64Array',
|
|
'Int8Array',
|
|
'Int16Array',
|
|
'Int32Array',
|
|
'Map',
|
|
'Set',
|
|
'WeakMap',
|
|
'Uint8Array',
|
|
'Uint8ClampedArray',
|
|
'Uint16Array',
|
|
'Uint32Array',
|
|
'ArrayBuffer',
|
|
'JSON',
|
|
'parseFloat',
|
|
'parseInt',
|
|
'console',
|
|
'isNaN',
|
|
'eval',
|
|
'isFinite',
|
|
'encodeURI',
|
|
'decodeURI',
|
|
'encodeURIComponent',
|
|
'decodeURIComponent',
|
|
|
|
// common method/property names of globals
|
|
'map',
|
|
'push',
|
|
'at',
|
|
'filter',
|
|
'slice',
|
|
'splice',
|
|
'add',
|
|
'get',
|
|
'set',
|
|
'has',
|
|
'size',
|
|
'length',
|
|
'toString',
|
|
]);
|
|
|
|
function AnonymizePlugin(_babel) {
|
|
let index = 0;
|
|
const identifiers = new Map();
|
|
const literals = new Map();
|
|
return {
|
|
name: 'anonymize',
|
|
visitor: {
|
|
JSXNamespacedName(path) {
|
|
throw error('TODO: handle JSXNamedspacedName');
|
|
},
|
|
JSXIdentifier(path) {
|
|
const name = path.node.name;
|
|
if (TAG_NAMES.has(name)) {
|
|
return;
|
|
}
|
|
let nextName = identifiers.get(name);
|
|
if (nextName == null) {
|
|
const isCapitalized =
|
|
name.slice(0, 1).toUpperCase() === name.slice(0, 1);
|
|
nextName = isCapitalized
|
|
? `Component${(index++).toString(16).toUpperCase()}`
|
|
: `c${(index++).toString(16)}`;
|
|
identifiers.set(name, nextName);
|
|
}
|
|
path.node.name = nextName;
|
|
},
|
|
Identifier(path) {
|
|
const name = path.node.name;
|
|
if (BUILTIN_HOOKS.has(name) || GLOBALS.has(name)) {
|
|
return;
|
|
}
|
|
let nextName = identifiers.get(name);
|
|
if (nextName == null) {
|
|
const isCapitalized =
|
|
name.slice(0, 1).toUpperCase() === name.slice(0, 1);
|
|
const prefix = isCapitalized ? 'V' : 'v';
|
|
nextName = `${prefix}${(index++).toString(16)}`;
|
|
if (name.startsWith('use')) {
|
|
nextName =
|
|
'use' + nextName.slice(0, 1).toUpperCase() + nextName.slice(1);
|
|
}
|
|
identifiers.set(name, nextName);
|
|
}
|
|
path.node.name = nextName;
|
|
},
|
|
JSXText(path) {
|
|
const value = path.node.value;
|
|
let nextValue = literals.get(value);
|
|
if (nextValue == null) {
|
|
let string = '';
|
|
while (string.length < value.length) {
|
|
string += String.fromCharCode(Math.round(Math.random() * 25) + 97);
|
|
}
|
|
nextValue = string;
|
|
literals.set(value, nextValue);
|
|
}
|
|
path.node.value = nextValue;
|
|
},
|
|
StringLiteral(path) {
|
|
const value = path.node.value;
|
|
let nextValue = literals.get(value);
|
|
if (nextValue == null) {
|
|
let string = '';
|
|
while (string.length < value.length) {
|
|
string += String.fromCharCode(Math.round(Math.random() * 58) + 65);
|
|
}
|
|
nextValue = string;
|
|
literals.set(value, nextValue);
|
|
}
|
|
path.node.value = nextValue;
|
|
},
|
|
NumericLiteral(path) {
|
|
const value = path.node.value;
|
|
let nextValue = literals.get(value);
|
|
if (nextValue == null) {
|
|
nextValue = Number.isInteger(value)
|
|
? Math.round(Math.random() * Number.MAX_SAFE_INTEGER)
|
|
: Math.random() * Number.MAX_VALUE;
|
|
literals.set(value, nextValue);
|
|
}
|
|
path.node.value = nextValue;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
let file;
|
|
let text;
|
|
if (argv.length >= 3) {
|
|
file = argv[2];
|
|
text = fs.readFileSync(file, 'utf8');
|
|
} else {
|
|
// read from stdin
|
|
file = 'stdin.js';
|
|
text = fs.readFileSync(stdin.fd, 'utf8');
|
|
}
|
|
const language =
|
|
file.endsWith('.ts') || file.endsWith('.tsx') ? 'typescript' : 'flow';
|
|
const result = runPlugin(text, file, language);
|
|
format(result, language).then(formatted => {
|
|
console.log(formatted);
|
|
});
|