Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38b39be914 | ||
|
|
1324e1bb1f |
@@ -988,7 +988,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'void-use-memo',
|
||||
description:
|
||||
'Validates that useMemos always return a value. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
|
||||
preset: LintRulePreset.RecommendedLatest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,21 +83,11 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
z.object({type: z.literal('name'), name: z.string()}),
|
||||
]);
|
||||
|
||||
// Would like to change this to drop the string option, but breaks compatibility with existing configs
|
||||
export const MacroSchema = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.array(MacroMethodSchema)]),
|
||||
]);
|
||||
export const MacroSchema = z.string();
|
||||
|
||||
export type CompilerMode = 'all_features' | 'no_inferred_memo';
|
||||
|
||||
export type Macro = z.infer<typeof MacroSchema>;
|
||||
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
/*
|
||||
@@ -659,7 +649,7 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* Invalid:
|
||||
* useMemo(() => { ... }, [...]);
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(false),
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
|
||||
@@ -438,40 +438,6 @@ export function dropManualMemoization(
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bailout on void return useMemos. This is an anti-pattern where code might be using
|
||||
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
|
||||
* values.
|
||||
*/
|
||||
if (
|
||||
func.env.config.validateNoVoidUseMemo &&
|
||||
manualMemo.kind === 'useMemo'
|
||||
) {
|
||||
const funcToCheck = sidemap.functions.get(
|
||||
fnPlace.identifier.id,
|
||||
)?.value;
|
||||
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
|
||||
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This ${
|
||||
manualMemo.loadInstr.value.kind === 'PropertyLoad'
|
||||
? 'React.useMemo()'
|
||||
: 'useMemo()'
|
||||
} callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instr.value = getManualMemoizationReplacement(
|
||||
fnPlace,
|
||||
instr.value.loc,
|
||||
@@ -629,17 +595,3 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
makeInstructionId,
|
||||
@@ -15,9 +14,35 @@ import {
|
||||
Place,
|
||||
ReactiveScope,
|
||||
} from '../HIR';
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {Macro} from '../HIR/Environment';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Whether a macro requires its arguments to be transitively inlined (eg fbt)
|
||||
* or just avoid having the top-level values be converted to variables (eg fbt.param)
|
||||
*/
|
||||
enum InlineLevel {
|
||||
Transitive = 'Transitive',
|
||||
Shallow = 'Shallow',
|
||||
}
|
||||
type MacroDefinition = {
|
||||
level: InlineLevel;
|
||||
properties: Map<string, MacroDefinition> | null;
|
||||
};
|
||||
|
||||
const SHALLOW_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Shallow,
|
||||
properties: null,
|
||||
};
|
||||
const TRANSITIVE_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: null,
|
||||
};
|
||||
const FBT_MACRO: MacroDefinition = {
|
||||
level: InlineLevel.Transitive,
|
||||
properties: new Map([['*', SHALLOW_MACRO]]),
|
||||
};
|
||||
FBT_MACRO.properties!.set('enum', FBT_MACRO);
|
||||
|
||||
/**
|
||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||
@@ -42,250 +67,210 @@ import {Iterable_some} from '../Utils/utils';
|
||||
* ## User-defined macro-like function
|
||||
*
|
||||
* Users can also specify their own functions to be treated similarly to fbt via the
|
||||
* `customMacros` environment configuration.
|
||||
* `customMacros` environment configuration. By default, user-supplied custom macros
|
||||
* have their arguments transitively inlined.
|
||||
*/
|
||||
export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
fn: HIRFunction,
|
||||
): Set<IdentifierId> {
|
||||
const fbtMacroTags = new Set<Macro>([
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
const macroKinds = new Map<Macro, MacroDefinition>([
|
||||
...Array.from(FBT_TAGS.entries()),
|
||||
...(fn.env.config.customMacros ?? []).map(
|
||||
name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
|
||||
),
|
||||
]);
|
||||
/**
|
||||
* Set of all identifiers that load fbt or other macro functions or their nested
|
||||
* properties, as well as values known to be the results of invoking macros
|
||||
* Forward data-flow analysis to identify all macro tags, including
|
||||
* things like `fbt.foo.bar(...)`
|
||||
*/
|
||||
const macroTagsCalls: Set<IdentifierId> = new Set();
|
||||
const macroTags = populateMacroTags(fn, macroKinds);
|
||||
|
||||
/**
|
||||
* Mapping of lvalue => list of operands for all expressions where either
|
||||
* the lvalue is a known fbt/macro call and/or the operands transitively
|
||||
* contain fbt/macro calls.
|
||||
*
|
||||
* This is the key data structure that powers the scope merging: we start
|
||||
* at the lvalues and merge operands into the lvalue's scope.
|
||||
* Reverse data-flow analysis to merge arguments to macro *invocations*
|
||||
* based on the kind of the macro
|
||||
*/
|
||||
const macroValues: Map<Identifier, Array<Identifier>> = new Map();
|
||||
// Tracks methods loaded from macros, like fbt.param or idx.foo
|
||||
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
|
||||
const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
|
||||
|
||||
visit(fn, fbtMacroTags, macroTagsCalls, macroMethods, macroValues);
|
||||
|
||||
for (const root of macroValues.keys()) {
|
||||
const scope = root.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
// Merge the operands into the same scope if this is a known macro invocation
|
||||
if (!macroTagsCalls.has(root.id)) {
|
||||
continue;
|
||||
}
|
||||
mergeScopes(root, scope, macroValues, macroTagsCalls);
|
||||
}
|
||||
|
||||
return macroTagsCalls;
|
||||
return macroValues;
|
||||
}
|
||||
|
||||
export const FBT_TAGS: Set<string> = new Set([
|
||||
'fbt',
|
||||
'fbt:param',
|
||||
'fbt:enum',
|
||||
'fbt:plural',
|
||||
'fbs',
|
||||
'fbs:param',
|
||||
'fbs:enum',
|
||||
'fbs:plural',
|
||||
const FBT_TAGS: Map<string, MacroDefinition> = new Map([
|
||||
['fbt', FBT_MACRO],
|
||||
['fbt:param', SHALLOW_MACRO],
|
||||
['fbt:enum', FBT_MACRO],
|
||||
['fbt:plural', SHALLOW_MACRO],
|
||||
['fbs', FBT_MACRO],
|
||||
['fbs:param', SHALLOW_MACRO],
|
||||
['fbs:enum', FBT_MACRO],
|
||||
['fbs:plural', SHALLOW_MACRO],
|
||||
]);
|
||||
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
'fbt:param',
|
||||
'fbs:param',
|
||||
]);
|
||||
|
||||
function visit(
|
||||
function populateMacroTags(
|
||||
fn: HIRFunction,
|
||||
fbtMacroTags: Set<Macro>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
macroValues: Map<Identifier, Array<Identifier>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const macroOperands: Array<Identifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
if (macroValues.has(operand.identifier)) {
|
||||
macroOperands.push(operand.identifier);
|
||||
}
|
||||
}
|
||||
if (macroOperands.length !== 0) {
|
||||
macroValues.set(phi.place.identifier, macroOperands);
|
||||
}
|
||||
}
|
||||
for (const instruction of block.instructions) {
|
||||
const {lvalue, value} = instruction;
|
||||
if (lvalue === null) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
value.kind === 'Primitive' &&
|
||||
typeof value.value === 'string' &&
|
||||
matchesExactTag(value.value, fbtMacroTags)
|
||||
) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
) {
|
||||
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
|
||||
macroMethods.set(lvalue.identifier.id, methods);
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroMethods.has(value.object.identifier.id)
|
||||
) {
|
||||
const methods = macroMethods.get(value.object.identifier.id)!;
|
||||
const newMethods = [];
|
||||
for (const method of methods) {
|
||||
if (
|
||||
method.length > 0 &&
|
||||
(method[0].type === 'wildcard' ||
|
||||
(method[0].type === 'name' && method[0].name === value.property))
|
||||
) {
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Map<IdentifierId, MacroDefinition> {
|
||||
const macroTags = new Map<IdentifierId, MacroDefinition>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'Primitive': {
|
||||
if (typeof value.value === 'string') {
|
||||
const macroDefinition = macroKinds.get(value.value);
|
||||
if (macroDefinition != null) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
* all `fbt` string literals in case they are used as a jsx tag.
|
||||
*/
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroTagsCalls.has(value.object.identifier.id)
|
||||
) {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
|
||||
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
|
||||
isFbtCallExpression(macroTagsCalls, value)
|
||||
) {
|
||||
macroTagsCalls.add(lvalue.identifier.id);
|
||||
macroValues.set(
|
||||
lvalue.identifier,
|
||||
Array.from(
|
||||
eachInstructionValueOperand(value),
|
||||
operand => operand.identifier,
|
||||
),
|
||||
);
|
||||
} else if (
|
||||
Iterable_some(eachInstructionValueOperand(value), operand =>
|
||||
macroValues.has(operand.identifier),
|
||||
)
|
||||
) {
|
||||
const macroOperands: Array<Identifier> = [];
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroValues.has(operand.identifier)) {
|
||||
macroOperands.push(operand.identifier);
|
||||
case 'LoadGlobal': {
|
||||
let macroDefinition = macroKinds.get(value.binding.name);
|
||||
if (macroDefinition != null) {
|
||||
macroTags.set(lvalue.identifier.id, macroDefinition);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
if (typeof value.property === 'string') {
|
||||
const macroDefinition = macroTags.get(value.object.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
const propertyDefinition =
|
||||
macroDefinition.properties != null
|
||||
? (macroDefinition.properties.get(value.property) ??
|
||||
macroDefinition.properties.get('*'))
|
||||
: null;
|
||||
const propertyMacro = propertyDefinition ?? macroDefinition;
|
||||
macroTags.set(lvalue.identifier.id, propertyMacro);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
macroValues.set(lvalue.identifier, macroOperands);
|
||||
}
|
||||
}
|
||||
}
|
||||
return macroTags;
|
||||
}
|
||||
|
||||
function mergeScopes(
|
||||
root: Identifier,
|
||||
scope: ReactiveScope,
|
||||
macroValues: Map<Identifier, Array<Identifier>>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
): void {
|
||||
const operands = macroValues.get(root);
|
||||
if (operands == null) {
|
||||
return;
|
||||
}
|
||||
for (const operand of operands) {
|
||||
operand.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.mutableRange);
|
||||
macroTagsCalls.add(operand.id);
|
||||
mergeScopes(operand, scope, macroValues, macroTagsCalls);
|
||||
}
|
||||
}
|
||||
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
? s === macro
|
||||
: macro[1].length === 0 && macro[0] === s,
|
||||
);
|
||||
}
|
||||
|
||||
function matchTagRoot(
|
||||
s: string,
|
||||
tags: Set<Macro>,
|
||||
): Array<Array<MacroMethod>> | null {
|
||||
const methods: Array<Array<MacroMethod>> = [];
|
||||
for (const macro of tags) {
|
||||
if (typeof macro === 'string') {
|
||||
continue;
|
||||
function mergeMacroArguments(
|
||||
fn: HIRFunction,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
macroKinds: Map<Macro, MacroDefinition>,
|
||||
): Set<IdentifierId> {
|
||||
const macroValues = new Set<IdentifierId>(macroTags.keys());
|
||||
for (const block of Array.from(fn.body.blocks.values()).reverse()) {
|
||||
for (let i = block.instructions.length - 1; i >= 0; i--) {
|
||||
const instr = block.instructions[i]!;
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'DeclareContext':
|
||||
case 'DeclareLocal':
|
||||
case 'Destructure':
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal':
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate':
|
||||
case 'StoreContext':
|
||||
case 'StoreLocal': {
|
||||
// Instructions that never need to be merged
|
||||
break;
|
||||
}
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const macroDefinition =
|
||||
macroTags.get(callee.identifier.id) ??
|
||||
macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
let macroDefinition;
|
||||
if (value.tag.kind === 'Identifier') {
|
||||
macroDefinition = macroTags.get(value.tag.identifier.id);
|
||||
} else {
|
||||
macroDefinition = macroKinds.get(value.tag.name);
|
||||
}
|
||||
macroDefinition ??= macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const scope = lvalue.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(lvalue.identifier.id);
|
||||
if (macroDefinition != null) {
|
||||
visitOperands(
|
||||
macroDefinition,
|
||||
scope,
|
||||
lvalue,
|
||||
value,
|
||||
macroValues,
|
||||
macroTags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const [tag, rest] = macro;
|
||||
if (tag === s && rest.length > 0) {
|
||||
methods.push(rest);
|
||||
for (const phi of block.phis) {
|
||||
const scope = phi.place.identifier.scope;
|
||||
if (scope == null) {
|
||||
continue;
|
||||
}
|
||||
const macroDefinition = macroTags.get(phi.place.identifier.id);
|
||||
if (
|
||||
macroDefinition == null ||
|
||||
macroDefinition.level === InlineLevel.Shallow
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
macroValues.add(phi.place.identifier.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (methods.length > 0) {
|
||||
return methods;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
value: InstructionValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
macroTagsCalls.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' &&
|
||||
macroTagsCalls.has(value.property.identifier.id))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
value: InstructionValue,
|
||||
): boolean {
|
||||
return (
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
macroTagsCalls.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxChild(
|
||||
macroTagsCalls: Set<IdentifierId>,
|
||||
lvalue: Place | null,
|
||||
value: InstructionValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
|
||||
lvalue !== null &&
|
||||
macroTagsCalls.has(lvalue.identifier.id)
|
||||
);
|
||||
return macroValues;
|
||||
}
|
||||
|
||||
function expandFbtScopeRange(
|
||||
@@ -298,3 +283,22 @@ function expandFbtScopeRange(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function visitOperands(
|
||||
macroDefinition: MacroDefinition,
|
||||
scope: ReactiveScope,
|
||||
lvalue: Place,
|
||||
value: InstructionValue,
|
||||
macroValues: Set<IdentifierId>,
|
||||
macroTags: Map<IdentifierId, MacroDefinition>,
|
||||
): void {
|
||||
macroValues.add(lvalue.identifier.id);
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (macroDefinition.level === InlineLevel.Transitive) {
|
||||
operand.identifier.scope = scope;
|
||||
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
|
||||
macroTags.set(operand.identifier.id, macroDefinition);
|
||||
}
|
||||
macroValues.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,16 +135,7 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
const valSplit = parsedVal.split('.');
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
maybeConfig[key] = [parsedVal.split('.')[0]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
@@ -125,7 +126,22 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
|
||||
if (!hasNonVoidReturn(body.loweredFunc.func)) {
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'useMemo() callbacks must return a value',
|
||||
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: body.loc,
|
||||
message: 'useMemo() callbacks must return a value',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -146,10 +162,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
|
||||
*/
|
||||
for (const loc of unusedUseMemos.values()) {
|
||||
errors.pushDiagnostic(
|
||||
voidMemoErrors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.VoidUseMemo,
|
||||
reason: 'Unused useMemo()',
|
||||
reason: 'useMemo() result is unused',
|
||||
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
@@ -160,6 +176,7 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
);
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -192,3 +209,17 @@ function validateNoContextVariableAssignment(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasNonVoidReturn(func: HIRFunction): boolean {
|
||||
for (const [, block] of func.body.blocks) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (
|
||||
block.terminal.returnVariant === 'Explicit' ||
|
||||
block.terminal.returnVariant === 'Implicit'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Unused useMemo()
|
||||
|
||||
This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.invalid-unused-usememo.ts:3:2
|
||||
1 | // @validateNoVoidUseMemo
|
||||
2 | function Component() {
|
||||
> 3 | useMemo(() => {
|
||||
| ^^^^^^^ useMemo() result is unused
|
||||
4 | return [];
|
||||
5 | }, []);
|
||||
6 | return <div />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:3:16
|
||||
1 | // @validateNoVoidUseMemo
|
||||
2 | function Component() {
|
||||
> 3 | const value = useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 4 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 5 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
6 | const value2 = React.useMemo(() => {
|
||||
7 | console.log('computing');
|
||||
8 | }, []);
|
||||
|
||||
Error: useMemo() callbacks must return a value
|
||||
|
||||
This React.useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
|
||||
|
||||
error.useMemo-no-return-value.ts:6:17
|
||||
4 | console.log('computing');
|
||||
5 | }, []);
|
||||
> 6 | const value2 = React.useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | console.log('computing');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 8 | }, []);
|
||||
| ^^^^^^^^^ useMemo() callbacks must return a value
|
||||
9 | return (
|
||||
10 | <div>
|
||||
11 | {value}
|
||||
```
|
||||
|
||||
|
||||
@@ -44,15 +44,23 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -44,15 +44,23 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.count || $[1] !== props.option) {
|
||||
let t1;
|
||||
if ($[3] !== props.count) {
|
||||
t1 = identity(props.count);
|
||||
$[3] = props.count;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
t0 = (
|
||||
<span>
|
||||
{fbt._(
|
||||
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
|
||||
[
|
||||
fbt._plural(identity(props.count), "count"),
|
||||
fbt._plural(t1, "count"),
|
||||
fbt._param(
|
||||
"option",
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Foo(t0) {
|
||||
const $ = _c(7);
|
||||
const $ = _c(13);
|
||||
const { name1, name2 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== name1 || $[1] !== name2) {
|
||||
@@ -50,19 +50,34 @@ function Foo(t0) {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== name2) {
|
||||
t3 = <b>{name2}</b>;
|
||||
$[5] = name2;
|
||||
$[6] = t3;
|
||||
if ($[5] !== name1 || $[6] !== t2) {
|
||||
t3 = <span key={name1}>{t2}</span>;
|
||||
$[5] = name1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== name2) {
|
||||
t4 = <b>{name2}</b>;
|
||||
$[8] = name2;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== name2 || $[11] !== t4) {
|
||||
t5 = <span key={name2}>{t4}</span>;
|
||||
$[10] = name2;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"{user1} and {user2} accepted your PR!",
|
||||
[
|
||||
fbt._param("user1", <span key={name1}>{t2}</span>),
|
||||
fbt._param("user2", <span key={name2}>{t3}</span>),
|
||||
],
|
||||
[fbt._param("user1", t3), fbt._param("user2", t5)],
|
||||
{ hk: "2PxMie" },
|
||||
);
|
||||
$[0] = name1;
|
||||
|
||||
@@ -29,20 +29,24 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(6);
|
||||
const { name, data, icon } = t0;
|
||||
let t1;
|
||||
if ($[0] !== data || $[1] !== icon || $[2] !== name) {
|
||||
let t2;
|
||||
if ($[4] !== name) {
|
||||
t2 = <Text type="h4">{name}</Text>;
|
||||
$[4] = name;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
t1 = (
|
||||
<Text type="body4">
|
||||
{fbt._(
|
||||
"{item author}{icon}{=m2}",
|
||||
[
|
||||
fbt._param(
|
||||
"item author",
|
||||
|
||||
<Text type="h4">{name}</Text>,
|
||||
),
|
||||
fbt._param("item author", t2),
|
||||
fbt._param(
|
||||
"icon",
|
||||
|
||||
|
||||
@@ -27,16 +27,21 @@ import fbt from "fbt";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== props.text) {
|
||||
const t1 = identity(props.text);
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <>{t1}</>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t0 = (
|
||||
<Foo
|
||||
value={fbt._(
|
||||
"{value}%",
|
||||
[fbt._param("value", <>{identity(props.text)}</>)],
|
||||
{ hk: "10F5Cc" },
|
||||
)}
|
||||
value={fbt._("{value}%", [fbt._param("value", t2)], { hk: "10F5Cc" })}
|
||||
/>
|
||||
);
|
||||
$[0] = props.text;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { fbt } from "fbt";
|
||||
|
||||
function Example(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = fbt._(
|
||||
"Outer Text {=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
|
||||
<Foo key="b" x={x}>
|
||||
{fbt._(
|
||||
"{=m1}",
|
||||
[
|
||||
fbt._implicitParam(
|
||||
"=m1",
|
||||
<Bar key="a">
|
||||
{fbt._("Inner Text", null, { hk: "32YB0l" })}
|
||||
</Bar>,
|
||||
),
|
||||
],
|
||||
{ hk: "23dJsI" },
|
||||
)}
|
||||
</Foo>,
|
||||
),
|
||||
],
|
||||
{ hk: "2RVA7V" },
|
||||
);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Foo({ x, children }) {
|
||||
"use no memo";
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({ children }) {
|
||||
"use no memo";
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ x: "Hello" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) Outer Text <div>Hello</div><span>Inner Text</span>
|
||||
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
import {fbt} from 'fbt';
|
||||
|
||||
function Example({x}) {
|
||||
// "Inner Text" needs to be visible to fbt: the <Bar> element cannot
|
||||
// be memoized separately
|
||||
return (
|
||||
<fbt desc="Description">
|
||||
Outer Text
|
||||
<Foo key="b" x={x}>
|
||||
<Bar key="a">Inner Text</Bar>
|
||||
</Foo>
|
||||
</fbt>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo({x, children}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<>
|
||||
<div>{x}</div>
|
||||
<span>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({children}) {
|
||||
'use no memo';
|
||||
return children;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{x: 'Hello'}],
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { Stringify, identity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(9);
|
||||
const { firstname, lastname } = t0;
|
||||
let t1;
|
||||
if ($[0] !== firstname || $[1] !== lastname) {
|
||||
let t2;
|
||||
if ($[3] !== firstname) {
|
||||
t2 = <Stringify key={0} name={firstname} />;
|
||||
$[3] = firstname;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== lastname) {
|
||||
t3 = <Stringify key={1} name={lastname} />;
|
||||
$[5] = lastname;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
t1 = fbt._(
|
||||
"Name: {firstname}, {lastname}",
|
||||
[
|
||||
fbt._param("firstname", t2),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
identity(
|
||||
fbt._("(inner){lastname}", [fbt._param("lastname", t3)], {
|
||||
hk: "1Kdxyo",
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
);
|
||||
$[0] = firstname;
|
||||
$[1] = lastname;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== t1) {
|
||||
t2 = <div>{t1}</div>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstname: "first", lastname: "last" }],
|
||||
sequentialRenders: [{ firstname: "first", lastname: "last" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Name: <div>{"name":"first"}</div>, (inner)<div>{"name":"last"}</div></div>
|
||||
@@ -0,0 +1,42 @@
|
||||
import fbt from 'fbt';
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' +
|
||||
fbt.param('lastname', <Stringify key={1} name={lastname} />),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -16,22 +16,25 @@ import {Stringify} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
import { Stringify } from "shared-runtime";
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -70,14 +73,24 @@ function Component(t0) {
|
||||
fbt._param(
|
||||
"firstname",
|
||||
|
||||
<Stringify key={0} name={firstname} />,
|
||||
identity(firstname),
|
||||
),
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt._("(inner fbt)", null, { hk: "36qNwF" })}
|
||||
</Stringify>,
|
||||
identity(
|
||||
fbt._(
|
||||
"(inner){lastname}",
|
||||
[
|
||||
fbt._param(
|
||||
"lastname",
|
||||
|
||||
identity(lastname),
|
||||
),
|
||||
],
|
||||
{ hk: "1Kdxyo" },
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
{ hk: "3AiIf8" },
|
||||
@@ -90,7 +103,7 @@ function Component(t0) {
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = <Stringify>{t1}</Stringify>;
|
||||
t2 = <div>{t1}</div>;
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
@@ -108,4 +121,4 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"children":"Name: , "}</div>
|
||||
(kind: ok) <div>Name: first, (inner)last</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
@@ -12,22 +12,25 @@ import {Stringify} from 'shared-runtime';
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
<div>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
fbt.param('firstname', identity(firstname)),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
identity(
|
||||
fbt(
|
||||
'(inner)' + fbt.param('lastname', identity(lastname)),
|
||||
'Inner fbt value'
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function Component(props) {
|
||||
const $ = _c(16);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, _temp);
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -46,7 +46,7 @@ function Component(props) {
|
||||
const groupName1 = t0;
|
||||
let t1;
|
||||
if ($[2] !== props) {
|
||||
t1 = idx.a(props, _temp2);
|
||||
t1 = idx.a(props, (__0) => __0.group.label);
|
||||
$[2] = props;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
@@ -108,12 +108,6 @@ function Component(props) {
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp2(__0) {
|
||||
return __0.group.label;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component(props) {
|
||||
const $ = _c(10);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = idx(props, _temp);
|
||||
t0 = idx(props, (_) => _.group.label);
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -74,9 +74,6 @@ function Component(props) {
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.group.label;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoVoidUseMemo
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
useMemo(() => {
|
||||
return [];
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
const value2 = React.useMemo(() => {
|
||||
console.log('computing');
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{value2}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
|
||||
console.log("computing");
|
||||
|
||||
console.log("computing");
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<div>
|
||||
{undefined}
|
||||
{undefined}
|
||||
</div>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoVoidUseMemo
|
||||
// @validateNoVoidUseMemo @loggerTestOnly
|
||||
function Component() {
|
||||
const value = useMemo(() => {
|
||||
console.log('computing');
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":18,"index":61},"end":{"line":5,"column":3,"index":87},"filename":"invalid-useMemo-return-empty.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":19},"end":{"line":7,"column":1,"index":107},"filename":"invalid-useMemo-return-empty.ts"},"fnName":"component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":1,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,3 +1,4 @@
|
||||
// @loggerTestOnly
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
@@ -22,7 +23,7 @@ function Component(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const item = props.item;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @validateNoVoidUseMemo:false
|
||||
function Component(props) {
|
||||
const item = props.item;
|
||||
const thumbnails = [];
|
||||
|
||||
@@ -6,6 +6,7 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
@@ -24,10 +25,18 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
if (props.cond) {
|
||||
let t0;
|
||||
bb0: {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
t0 = props.value;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
t0 = undefined;
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,6 +2,7 @@ function Component(props) {
|
||||
const x = useMemo(() => {
|
||||
if (props.cond) {
|
||||
if (props.cond) {
|
||||
return props.value;
|
||||
}
|
||||
}
|
||||
}, [props.cond]);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
let x = useMemo(() => {
|
||||
mutate(a);
|
||||
}, []);
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function component(a) {
|
||||
mutate(a);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -120,7 +120,15 @@ testRule('plugin-recommended', TestRecommendedRules, {
|
||||
|
||||
return <Child x={state} />;
|
||||
}`,
|
||||
errors: [makeTestCaseError('Unused useMemo()')],
|
||||
errors: [
|
||||
makeTestCaseError('useMemo() callbacks must return a value'),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Pipeline errors are reported',
|
||||
|
||||
Reference in New Issue
Block a user