packages/babel-plugin-fbt/src/fbt-nodes/FbtParamNode.js (174 lines of code) (raw):
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @emails oncall+i18n_fbt_js
* @flow strict-local
* @format
*/
/*eslint max-len: ["error", 100]*/
'use strict';
import type {ParamVariationType} from '../../../../runtime/shared/FbtRuntimeTypes';
import type {
BabelNodeCallExpressionArg,
BabelNodeCallExpressionArgument,
} from '../FbtUtil';
import type {StringVariationArgsMap} from './FbtArguments';
import type {FromBabelNodeFunctionArgs} from './FbtNodeUtil';
const {ValidParamOptions} = require('../FbtConstants');
const {
collectOptionsFromFbtConstruct,
createFbtRuntimeArgCallExpression,
enforceBabelNodeExpression,
errorAt,
varDump,
} = require('../FbtUtil');
const {GENDER_ANY, NUMBER_ANY} = require('../translate/IntlVariations');
const {
GenderStringVariationArg,
NumberStringVariationArg,
} = require('./FbtArguments');
const FbtNode = require('./FbtNode');
const FbtNodeType = require('./FbtNodeType');
const {
createInstanceFromFbtConstructCallsite,
tokenNameToTextPattern,
} = require('./FbtNodeUtil');
const {
arrayExpression,
isExpression,
isStringLiteral,
numericLiteral,
stringLiteral,
} = require('@babel/types');
const invariant = require('invariant');
const nullthrows = require('nullthrows');
type Options = {|
gender?: ?BabelNodeExpression, // Represents the `gender`
name: string, // Name of the string token
// If `true`, the string that uses this fbt:param will have number variations.
// The `number` value will be inferred from the value of fbt:param
// If `number` is a `BabelNode`, then we'll use it internally as the value to determine
// the number variation, and the fbt:param value will represent the UI text to render.
number?: ?true | BabelNodeExpression,
value: BabelNodeCallExpressionArgument,
|};
/**
* Variations.
*/
const ParamVariation: ParamVariationType = {
number: 0,
gender: 1,
};
/**
* Represents an <fbt:param> or fbt.param() construct.
* @see docs/params.md
*/
class FbtParamNode extends FbtNode<
GenderStringVariationArg | NumberStringVariationArg,
BabelNodeCallExpression,
null,
Options,
> {
static +type: FbtNodeType = FbtNodeType.Param;
getOptions(): Options {
try {
const rawOptions = collectOptionsFromFbtConstruct(
this.moduleName,
this.node,
ValidParamOptions,
);
const [arg0, arg1] = this.getCallNodeArguments() || [];
const gender = enforceBabelNodeExpression.orNull(rawOptions.gender);
const number =
typeof rawOptions.number === 'boolean'
? rawOptions.number
: enforceBabelNodeExpression.orNull(rawOptions.number);
invariant(
number !== false,
'`number` option must be an expression or `true`',
);
invariant(
!gender || !number,
'Gender and number options must not be set at the same time',
);
let name = typeof rawOptions.name === 'string' ? rawOptions.name : null;
if (name == null || name === '') {
invariant(
isStringLiteral(arg0),
'First function argument must be a string literal',
);
name = arg0.value;
}
invariant(name.length, 'Token name string must not be empty');
const value = nullthrows(
arg1,
'The second function argument must not be null',
);
return {
gender,
name,
number,
value,
};
} catch (error) {
throw errorAt(this.node, error);
}
}
/**
* Create a new class instance given a BabelNode root node.
* If that node is incompatible, we'll just return `null`.
*/
static fromBabelNode({
moduleName,
node,
}: FromBabelNodeFunctionArgs): ?FbtParamNode {
return createInstanceFromFbtConstructCallsite(moduleName, node, this);
}
getArgsForStringVariationCalc(): $ReadOnlyArray<
GenderStringVariationArg | NumberStringVariationArg,
> {
const {gender, number} = this.options;
const ret = [];
invariant(
!gender || !number,
'Gender and number options must not be set at the same time',
);
if (gender) {
ret.push(new GenderStringVariationArg(this, gender, [GENDER_ANY]));
} else if (number) {
ret.push(
new NumberStringVariationArg(this, number === true ? null : number, [
NUMBER_ANY,
]),
);
}
return ret;
}
getTokenName(_argsMap: StringVariationArgsMap): string {
return this.options.name;
}
getText(argsMap: StringVariationArgsMap): string {
try {
this.getArgsForStringVariationCalc().forEach(expectedArg => {
const svArg = argsMap.get(this);
invariant(
// $FlowExpectedError[method-unbinding] We're just comparing methods by reference
svArg.constructor === expectedArg.constructor,
'Expected SVArgument instance of %s but got %s instead: %s',
expectedArg.constructor.name || 'unknown',
svArg.constructor.name || 'unknown',
varDump(svArg),
);
});
return tokenNameToTextPattern(this.getTokenName(argsMap));
} catch (error) {
throw errorAt(this.node, error);
}
}
getFbtRuntimeArg(): BabelNodeCallExpression {
const {gender, name, number, value} = this.options;
let variationValues;
if (number != null) {
variationValues = [numericLiteral(ParamVariation.number)];
if (number !== true) {
// For number="true" we don't pass additional value.
variationValues.push(number);
}
} else if (gender != null) {
variationValues = [numericLiteral(ParamVariation.gender), gender];
}
return createFbtRuntimeArgCallExpression(
this,
[
stringLiteral(name),
value,
variationValues ? arrayExpression(variationValues) : null,
].filter(Boolean),
);
}
getArgsThatShouldNotContainFunctionCallOrClassInstantiation(): $ReadOnly<{
[argName: string]: BabelNodeCallExpressionArg,
}> {
const {gender, number} = this.options;
if (gender != null) {
return {gender};
}
return isExpression(number) ? {number} : {};
}
}
module.exports = FbtParamNode;