My Linter Algorithm to detect non-internationalised Variables and Function Calls in codebase
This article is an explanation of how I solved a Linter problem task given to me at work. I spent almost one week on this task and even dreamed of the problem in my sleep. Because of the huge amount of time I spent working on it, I want to document my solution. Maybe it is not the best solution, but with my current intellectual capacity, it is the best I could think of ;)
Introduction to problem:
The task is to be able to detect hardcoded UI variable and function calls in Typescript React code.
Variables or function calls that are wrapped in JSX expressions such as div has to be wrapped in gettext if they contain strings. In the following example, we see that the value of getUIString is a string. This string needs to be wrapped in gettext to be counted as internationalised. In this case, it is not. Hence, the job of the linter is to detect the presence of this non-internalised string and flag an error.
The same check has to be applied to variables wrapped in JSX expressions.
function getUIString() {
return 'My UI string';
}
return (
<div> { getUIString() }</div>
);
(Really) Helpful resources:
- @typescript-eslint: A library that I used to help me because the default eslint library is for linting Javascript code but the code I want to lint is Typescript.
- https://eslint.org/docs/developer-guide/
- https://astexplorer.net/
This article does not explain how to write a Linter plugin. It assumes that readers already know. If not, please refer to tutorials such as: https://blog.devgenius.io/how-to-write-a-es-lint-rule-2abdd9bfd327 or look out at another article of mine.
Rule Strategy:
Traces the code execution path of referenced UI variables / function calls to determine if the last assigned value is a valid or invalid value.
Introduction of data structure and types used throughout rule code:
The following 6 data structures will be populated with the relevant information to help with the validation of the code.
// Store all the validated nodes with proper gettext
const translatedNodes = new Set<TSESTree.Node>();// Store all the invalid imports nodes: External Library import
const invalidNodes = new Set<TSESTree.Node>();// Store all the variable/function UI nodes
const UINodes: TSESTree.Node[] = [];// Maps a node to all the identifiers that it contains
const nodeContainsIdentifiers: Map<TSESTree.Node, Set<Identifier>> = new Map();// Maps an identifier to all the values that it has been assigned to in the code
const identifierAssignments: Map<string, Array<Assignment>> = new Map();// Tracks the range of the nodes we have visited during validation so that we do not fall into an infinite loop
// Example: In the case that we have stmt such as:
// (Line 1) let a = gettext("good");
// (Line 2) a = a + "string", we will not want to fall into the infinite loop of visiting the same node at Line 2 repeatedly
const visited: Set<number> = new Set();
nodeContainsIdentifiers and identifierAssignments are actually quite similar. They are almost the reverse maps of each other. I am still wondering if there is anyway i can do to improve the code. It seems quite confusing to have the two of them ….
Here are the extra class types created to help in the management of data storage.
export class Assignment {
public range: TSESTree.Range;
public node: DeclarationNode;
constructor(range: TSESTree.Range, node: DeclarationNode) {
this.range = range;
this.node = node;
}
}
export class StackNode {
public identifierName: string;
public currStartChar: number;
constructor(identifierName: string, currStartChar: number) {
this.identifierName = identifierName;
this.currStartChar = currStartChar;
}
}
export class Identifier {
public identifier: string;
public range: TSESTree.Range;
constructor(identifier: string, range: TSESTree.Range) {
this.identifier = identifier;
this.range = range;
}
}
Step 1: Detect declaration / assignment expression nodes
The goal of this step is to populate the data structure identifierAssignments.
Firstly, I created four functions which will be called whenever a node that matches their mapped selector is found. The name of the four functions are handleFunctionDeclaration, handleVariableDeclaration, handleImportDeclaration, handleAssignmentExpression. Refer to this link for more guide on selectors: https://eslint.org/docs/developer-guide/selectors
// Examples:
// function a() {}
// Handle function declaration
FunctionDeclaration: handleFunctionDeclaration,
// Examples:
// const variable = "";
// let variable = () => {}
// To detect variables declared
VariableDeclaration: handleVariableDeclaration,
// Detect imports
// Example: import { getUIString, hello } from "someLibrary";
ImportDeclaration: handleImportDeclaration,
// Examples:
// variableDeclaredBefore = "";
// variableDeclaredBefore = () => {}
// To detect assignments that are not declarations (eg, don't have let or const before variable)
'ExpressionStatement > AssignmentExpression': handleAssignmentExpression,
For example, if the code contains a line: const a = b; , the function handleVariableDeclaration will be called. In this function, it will receive this declarator node (AST form) as a parameter. The code will then extract the identifier name of this expression, in this case will be a. It will also extract the character position that this declaration node appears in the code as well as extract the entire AST of the node itself.
function handleVariableDeclaration(node: TSESTree.VariableDeclaration): void {
// @ts-ignore
const identifier = node.declarations[0].id.name;
const rangeIdentifier = node.declarations[0].id.range;
const nodeDeclarator = node.declarations[0];
// Store the pairing of identifier with it's assigned value
setIdentifierAssignments(identifier, rangeIdentifier, nodeDeclarator);
}
After extraction, these 3 key information will be stored in a data structure called identifierAssignments.
const identifierAssignments: Map<string, Array<Assignment>> = new Map();
The key of this map will be the name of either the variable being assigned a value / a function being declared etc etc, in this case it is a. The value of the map is an array of Assignment. Assignment is a self defined class type and it will store the latter two extracted information.
Overtime, this map will give us the association of identifiers (aka variable name/function name/Import name) with all of the nodes they have been assigned as their values in the code.
The logic mentioned above is applied for all four functions: handleFunctionDeclaration, handleVariableDeclaration, handleImportDeclaration, handleAssignmentExpression.
handleImportDeclaration has a few more additional steps. As shared above, there is a data structure called translatedNodes to store all the nodes that have been defined to be valid. In this function, i will add all the imported nodes into translatedNodes. I will also set the data structure nodeContainsIdentifiers so that the import node will be associated with the identifiers present.
// Store all the validated nodes with proper gettext
const translatedNodes = new Set<TSESTree.Node>();// Maps a node to all the identifiers that it contains
const nodeContainsIdentifiers: Map<TSESTree.Node, Set<Identifier>> = new Map();
For eg, if the statement is “import { getUIString, hello } from “someLibrary”;”,
The import node will be associated with getUIString and hello so that later on, during validation, we will be able to see that these two identifiers are mapped to a node that is present in translatedNodes, which then indicates these two identifiers are valid.
Step 2: Collect UI Identifiers
// Function in JSX expression
// Example: <div>{ getSomeUIString() }</div>
':matches(JSXElement, JSXFragment) > JSXExpressionContainer CallExpression > Identifier': handleUIIdentifier,// Variable in JSX expression
// Example: <div>{ someVarDefinedAbove }</div>
':matches(JSXElement, JSXFragment) > JSXExpressionContainer > Identifier': handleUIIdentifier,
Secondly, I created another function handleUIIdentifier to map with variables or function calls appearing inside a JSX expression. The function is very simple. It’s goal is to collect all the UI identifiers into the data structure called UINodes. When we start doing the actual validation, we will traverse this data structure and validate each node one by one.
// Store all the variable/function UI nodes
const UINodes: TSESTree.Node[] = [];.....
.....
.....function handleUIIdentifier(node: TSESTree.Identifier): void {
UINodes.push(node);
}
Step 3: Handle Literals
The goal of this step is to populate invalidNodes.
Now, the key of this rule is to find out whether a variable / function calls contains hardcoded string / literal values. We want to flag nodes containing such forbidden literals as invalid by adding them into the data structure, invalidNodes.
// Store all the invalid imports nodes: External Library import
const invalidNodes = new Set<TSESTree.Node>();
However, we need to be careful about how we detect literals and flag invalid nodes appropriately. Consider the following different scenarios:
const a = "bad string"; // node a here should be marked incorrect
// Although there is a literal here, we should not mark node b incorrect because the literal is wrapped in gettext.
let b = gettext("good string");
b = "bad"; // node b here should be marked incorrect// We should mark all ancestors of the literal as incorrect.
function c() {return "bad function string";} // We do not need to mark all ancestor of the literal, sly fox, as incorrect. Only need to mark node e declarator node as incorrect. This is because the return value of function d does not involve e.
function d() { const e = "sly fox";
return gettext("valid");}// We need to mark all ancestors of the literal as incorrect because return value of f() is invalid. We want f declarator marked incorrect.
function f () { let g = "colourful";
return g + "rainbow";}// Same as above
const h = () => ("bad string");const j = () => {return "bad string"};
It is a pain to figure out how to write the selectors for all the different cases….but here are the ones I came up with.
// Hardcoded literals excluding those inside gettext or import libraries or JSX attribute
// Example of excluded texts:
// gettext("hello"), import { getUIString, hello } from "someLibrary";, <div atrr="hh">{a}<div>
// ':not(CallExpression, ImportDeclaration, JSXAttribute) > Literal':
// Literal can be within a function scope or outside
':not(CallExpression, ImportDeclaration, JSXAttribute) > Literal': handleInvalidLiteral,// Hardcoded literals (together with other addition) inside ReturnStatement of a function
// function b () {return "s" + a;} const b = () => {return "a"+ a;}
'ReturnStatement :not(CallExpression, ImportDeclaration, JSXAttribute) Literal': handleInvalidLiteralInReturn,// Hardcoded literals (Just 1) inside ReturnStatement of a function
// Example: const b = () => {return "s";} function b () {return "s";}
'ReturnStatement > Literal': handleInvalidLiteralInReturn,// Hardcoded literals inside a function with only 1 stmt
// Example: const c = () => (b() + "bad string");
// We treat them the same as if they were inside a ReturnStatement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BinaryExpression > Literal': handleInvalidLiteralInReturn,// Hardcoded literals inside a function with only 1 stmt
// Example: const c = () => ("bad string");
// We treat them the same as if they were inside a ReturnStatement
':matches(ArrowFunctionExpression, FunctionDeclaration) > Literal': handleInvalidLiteralInReturn,
In general, I created 2 functions: handleInvalidLiteral and handleInvalidLiteralInReturn.
handleInvalidLiteral will be triggered for ALL literals present in the code with the exception of the following three cases:
- Literal is not wrapped in CallExpression (meaning, not wrapped in a function such as gettext(“string”))
- Literal is not part of an Import Declaration (so that the library strings inside quotations such as someLibrary inside the quote … from “someLibrary” will not be detected)
- Literal is not within a JSXAttribute (such as “hh” inside <div className=”hh”>{a}<div>)
This is the code:
function handleInvalidLiteral(node: TSESTree.Literal): void {
invalidNodes.add(node);
const ancestors = context.getAncestors();
while (ancestors.length > 0) {
const parent = ancestors.pop();
if (parent !== undefined && !isNodeFunction(parent)) {
// Only mark ancestors as invalid until function level
invalidNodes.add(parent);
} else {
break;
}
}
}]// Helper class
function isNodeFunction(node: TSESTree.Node): boolean {
return node.type === 'FunctionDeclaration'
|| node.type === 'ArrowFunctionExpression';
}
As the presence of a literal indicates the node is invalid, in the function, I added the node into the data structure invalidNodes.
Additionally, I also marked all their ancestors as invalid, though stopping at the function level ancestor and beyond.
For eg, if the code is:
function f () {
let g = "colourful";
return g;
}
From the innermost Literal node “colourful”, we can view all it’s ancestor nodes -> Go outwards. I will mark it’s ancestors: VariableDeclarator, VariableDeclaration, BlockStatement as invalid by adding to the data structure invalidNodes. However, I will stop at ancestor FunctionDeclaration and beyond. The reason being that, I am not able to tell if the literal will affect the value of the function because the variable g that the literal is assigned to, might not be part of the return statement. (Though in this case it is part of the return statement).
But in other cases such as below, where the literal “sly fox” is assigned to variable e but variable e is not part of the return value of function d(), we should not mark function d() as invalid as it is valid.
// (Example 4 of all the given scenarios above)function d() {const e = "sly fox";
return gettext("valid");}
For nodes containing literals and not wrapped in function, this method handleInvalidLiteral will mark all it’s ancestors, including it’s declarator or assignment node, as invalid.
Note that here, we do not need to care if the hardcoded literal is part of a binary expression, together with another variable or another gettext string because once there is a hardcoded literal present, we can be sure to mark all it’s ancestors as invalid.
But we still need a way to mark a function as invalid if it’s return value is invalid. (which we have not done so with handleInvalidLiteral). So, here comes handleInvalidLiteralInReturn.
handleInvalidLiteralInReturn will deal with all the literals present in the returnStatement.
The logic for it is simple. It simply marks all the literal’s ancestor nodes as invalid because once the return value of a function is a literal, the function it is contained is, will also be invalid.
function handleInvalidLiteralInReturn(node: TSESTree.Literal): void {
invalidNodes.add(node);
const ancestors = context.getAncestors();
// Mark all ancestors as invalid
ancestors.forEach((anc) => invalidNodes.add(anc));
}
Now, invalidNodes is filled up with all the variable declarators / variable assignments and function declarators that are all DEFINITELY invalid — has literals.
Step 4: Time to handle Identifiers
// Function calls, referencing a variable
// Example: <div>{ identifierVar }</div>, identifierFnA(), someObj.identifierFnB()
Identifier: handleIdentifier,
// Identifier (Not inside ReturnStatement) inside a function body with more than 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BlockStatement[body.length>1] Identifier': handleIdentifiersInFunctionBody,
// Identifier inside return value of function body containing more than 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BlockStatement[body.length>1] ReturnStatement Identifier': handleIdentifiersInReturn,
// Identifier inside return value of a function body containing only 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BlockStatement[body.length=1] ReturnStatement Identifier': handleIdentifiersInReturn,
// Identifier inside a function body containing only 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > :not(BlockStatement) Identifier': handleIdentifiersInReturn,
To clarify, identifiers are all variables or function names. Those highlighted below are Identifiers.
const a = "hi";
let b = 3;
b = 45;function c() {
return b;}const d = () => {c()};const f = gettext("good string");
Here, there are three main functions: handleIdentifier, handleIdentifiersInFunctionBody and handleIdentifiersInReturn.
The concept is quite similar to step 3 except that the goal here is concerned with populating translatedNodes and nodesContainsIdentifiers.
The method handleIdentifier will only handle Identifiers not enclosed within a function body. I had a lot of trouble coming up with the selector to match this scenario and hence you can see that the selector mapped to this function is for all identifiers…`Identifier: handleIdentifier`
Only inside the handleIndentifier function, the code will check to see if it has any function ancestor. If yes, we know that the identifier is enclosed within a function body and the code will exit.
function handleIdentifier(node: TSESTree.Identifier): void {
const { name } = node;
const ancestors = context.getAncestors();
const nodeHasFunctionAncestor = ancestors.filter((anc) => isNodeFunction(anc));
if (nodeHasFunctionAncestor.length > 0) return;
if (gettextFunctionNames.has(name)) {
markAncestorsTranslated();
return;
}
// Skip if the identifier is found on the LHS of an assignment / declarator
// We do not want to say that the ancestor nodes' contain this identifier as a value
if (!isIdentifierOnLHSOfAssignOrDeclare(node)) {
associateIdentifierWithAllAncestors(name, node);
}
}
Otherwise, the code will go on to check if the identifier matches with any of the “correct” wrapper. gettextFunctionNames contains a list of all the accepted forms.
For example, const f = gettext(“good string”); when the function received gettext as an identifier, it knows that it’s declaration node is likely to be valid and calls the function markAncestorsTranslated which does the work of populating translatedNodes.
Bear in mind though…if we have lines like: const f = gettext(“good string”) + “bad string”; or const amIValid = gettext(“good string”) + c; It would be wrong to mark the variable declarator node f as translated — because there is a literal or potentially invalid variable assigned to the declarator. So, we need to check whether the identifier node is within a binary expression and account for those cases.
function markAncestorsTranslated(): void {
const ancestors = context.getAncestors();
const nodeIsInBinaryExpr = ancestors.filter((anc) => isNodeBinaryExpression(anc));
if (nodeIsInBinaryExpr.length > 0) {
// If the node is within a binary expression, cannot mark entire node as valid
markTillBinaryExprTranslated();
} else {
markAllAncestorsTranslated();
}
}// Helper
function isNodeBinaryExpression(node: TSESTree.Node): boolean {
return node.type === 'BinaryExpression';
}function markAllAncestorsTranslated(): void {
const ancestors = context.getAncestors();
ancestors.forEach((ancestorNode) => {
translatedNodes.add(ancestorNode);
});
}
function markTillBinaryExprTranslated(): void {
const ancestors = context.getAncestors();
while (ancestors.length > 0) {
const parent = ancestors.pop();
if (!parent) continue;
if (isNodeBinaryExpression(parent)) return;
translatedNodes.add(parent);
}
}
Going on to the third part of the function handleIdentifier
// Skip if the identifier is found on the LHS of an assignment / declarator
// We do not want to say that the ancestor nodes' contain this identifier as a value
if (!isIdentifierOnLHSOfAssignOrDeclare(node)) {
associateIdentifierWithAllAncestors(name, node);
}....
....
....
....function associateIdentifierWithAllAncestors(name: string, node: TSESTree.Node): void {
const ancestors = context.getAncestors();
ancestors.forEach((anc) => {
// Associate current identifier with all of its' ancestor node
setNodeContainsIdentifiers(name, node, anc);
});
}
We will then populate nodesContainsIdentifiers. This is to associate the ancestor nodes with the specific identifier we are currently at.
Next, handleIdentifiersInFunctionBody function will deal with identifiers inside a function body, excluding identifiers inside ReturnStatement of a function. We will deal with those separately.
handleIdentifiersInFunctionBody will check if the identifier is in a valid translated form. If yes, it will mark all it’s ancestors except until function level as translated by adding them to translatedNodes. (Same concept as what was explained for adding ancestors to invalid nodes). It will also check if any of it’s ancestor node is a binary expression. (Same concept as what was explained for adding ancestors to invalid nodes).
If the identifier is not in valid translated form, the code will go on to populate nodesContainsIdentifiers to associate current identifier with each of it’s ancestor node except for function ancestors and beyond.
function handleIdentifiersInFunctionBody(node: TSESTree.Identifier): void {
const { name } = node;
if (gettextFunctionNames.has(name)) {
// Only mark ancestors in the same line as safe
// Ignore ancestor function nodes
const ancestorsOne = context.getAncestors();
while (ancestorsOne.length > 0) {
const parent = ancestorsOne.pop();
if (!parent) continue;
if (isNodeBinaryExpression(parent) || isNodeFunction(parent)) break;
translatedNodes.add(parent);
}
return;
}
// By doing pop, we are traversing outwards
// from the innermost identifier node to each of it's parent
const ancestors = context.getAncestors();
while (ancestors.length > 0) {
const parent = ancestors.pop();
if (parent !== undefined && !isNodeFunction(parent)) {
// Associate current identifier with each of it's ancestor node except for function ancestors
// and above
setNodeContainsIdentifiers(name, node, parent);
} else {
break;
}
}
}
The last function to deal with identifiers inside Return Statements is handleIdentifiersInReturn. The things it does is pretty similar to the above, although with some slight modifications.
// Handle identifiers which is either the sole statement in the function body
// or is located in the ReturnStatement
function handleIdentifiersInReturn(node: TSESTree.Identifier): void {
const { name } = node;
if (gettextFunctionNames.has(name)) {
markAncestorsTranslated();
return;
}
associateIdentifierWithAllAncestors(name, node);
}function associateIdentifierWithAllAncestors(name: string, node: TSESTree.Node): void {
const ancestors = context.getAncestors();
ancestors.forEach((anc) => {
// Associate current identifier with all of its' ancestor node
setNodeContainsIdentifiers(name, node, anc);
});
}
A short breather
In case it was too much information for you, here is a brief summary.
Step 1: Detect declaration / assignment expression nodes
- Populate the data structure identifierAssignments where we map each identifier to an array of all it’s assigned values.
Step 2: Collect UI Identifiers (Those enclosed in JSX expressions)
- Populate UINodes so we can traverse this list of nodes to validate in step 5.
Step 3: Handle Literals (hardcoded strings)
- Populate invalidNodes where we add all the literals’ ancestor nodes.
- Need to be careful of where the literals are positioned. This will affect which ancestor nodes should be added to invalidNodes.
Step 4: Handle Identifiers (variables/function names)
- Populate translatedNodes and nodesContainsIdentifier. Check if identifiers are in the list of allowable gettext wrappers. If yes, add it’s ancestor nodes into translatedNodes. Otherwise, go on to the step of populating nodesContainsIdentifier to match all the identifier’s ancestor nodes to the identifier. -> so that later on, we know what identifiers are present in what nodes.
- Need to be careful of where the identifiers are positioned and whether they are part of a binary expression. This will affect which ancestor nodes should be added to translatedNodes.
If it is still confusing, i suggest to look at Step 5 .
Note that the four steps are independent of each other and there is no fixed ordering that they must execute. They can execute any time or in parallel even!
Step 5: Start evaluation
'Program:exit': evaluateNodes,
Here, the selector indicates that the function evaluateNode will be triggered only when the parser has finished parsing the program. This means, we will only call this function when all our data structures have been populated with all the values of the program.
You will see why we needed to store all those information now.
const messages = {
variableNeedI18n:
'i18n: Variables used in the UI should be wrapped with gettext functions',
functionNeedI18n:
'i18n: Functions used in the UI should be wrapped with gettext functions',
};
.....
.....
.....
.....
function evaluateNodes(): void {
UINodes.forEach((node) => {
const exitError = validateUINode(node);
if (exitError) {
if (node.parent && node.parent.type === 'CallExpression') {
context.report({
node,
messageId: 'functionNeedI18n' as TMessageIds,
});
return;
}
// Variable reference
context.report({
node,
messageId: 'variableNeedI18n' as TMessageIds,
});
}
});
}
evaluateNodes simply iterates through UINodes and calls validateUINode method. Depending on the exit error and node type, errors will or will not be raised. Errors are raised through context.report function.
What does validateUINode do?
To validate a UI node, we will need to search for it’s latest assignment value and determine if that is valid. We do this by doing a DFS search backwards. From the UI node, we keep “traversing backwards” to all of the nodes with the same identifier name (because these nodes tells us what value the UI node will eventually be assigned to) until we reach a node which can tell us whether the UI node is valid or invalid. We make use of a stack to help us.
Example:
const a = "hello";
const c = gettext("good");
let b = a;
b = b + c;
<div>{ b }</div>
The UI node here is b. From b, we need to trace upwards to eventually node a and c to determine if the UI node value is valid.
- Starting from the given UI node, obtain the nearest node above it that has the same identifier name. -> That would tell us the latest assignment.
- The nearest node is obtained by retrieving all the nodes associated with the identifier name in question from identifierAssignments.
- From all these nodes, compare their range value (which will tell us which position of the code section they appear). The nearest node will be the node with the biggest range value which indicates it is positioned lower down in the code and hence, is the latest node right before the UI node or any other node we are traversing at.
- If it is found that the nearest node belongs to a function declarator: Eg, function name() {}, then we will just return that function declarator as the nearest node. This is under the assumption that names of function declarators are not duplicated nor reassigned.
- Note, const a = () => {} is not counted as a function declarator.
- (Check 1) Otherwise, check if the nearest node is stored in invalidNodes, which will indicate whether it’s value is invalid. If yes, terminate with error.
- (Check 2) If the nearest node is stored in translatedNodes, it means that the value it contains is valid and that is the latest value that has been assigned to the node of interest. No need to continue validation for current node. If we are already in the middle of the traversal where we have gone through many nodes already, go back to step 1 and the code will check if there are any nodes in the stack to be checked.
- (Check 3) Check against the data structure nodeContainsIdentifiers to see if the nearest node contains any identifiers of interest. Retrieve all the identifiers and add their details into stack for next round of validation.
function validateUINode(node: TSESTree.Node): boolean {
if (node.type !== 'Identifier') {
return false;
}
const stack: [StackNode] = [new StackNode(node.name, node.range[0])];
while (stack.length > 0) {
const currNode = stack.pop();
if (currNode === undefined) continue;
const nearestNodeInfo = obtainNearestNode(currNode);
// Identifier has not been assigned to any values that are marked as invalid
// Scenario: Component
if (nearestNodeInfo === undefined) {
console.log('No next node retrieved for', currNode.identifierName);
continue;
}
if (invalidNodes.has(nearestNodeInfo.node)) {
console.log('Nearest node is an invalid node');
return true;
}
// The latest assignment has been found to be valid
if (translatedNodes.has(nearestNodeInfo.node)) {
console.log('Translated node has node');
continue;
}
// Obtain the identifier of that node we are going to visit next
if (nodeContainsIdentifiers.has(nearestNodeInfo.node)) {
const nextIdentifierNamesSet = nodeContainsIdentifiers.get(
nearestNodeInfo.node
);
// @ts-ignore
nextIdentifierNamesSet.forEach((next) => {
stack.push(new StackNode(next.identifier, next.range[0]));
});
}
}
return false;
}function obtainNearestNode(currNode: StackNode): Assignment | undefined {
const identifierName = currNode.identifierName;
const currStartChar = currNode.currStartChar;
// Get all the Nodes that the identifier has been assigned to
const nodeAssignments: Array<Assignment> | undefined = identifierAssignments.get(
identifierName
);
// Identifier not assigned any value
if (nodeAssignments === undefined) return undefined;
// Identifier is the identifier of a Function declaration
// There is only 1 Function declaration for each identifier and we can directly return the node found.
if (
nodeAssignments.length === 1 &&
nodeAssignments[0].node.type === 'FunctionDeclaration'
)
return new Assignment(nodeAssignments[0].range, nodeAssignments[0].node);
let nextStartChar = 0;
let assignment;
for (let i = 0; i < nodeAssignments.length; i++) {
const otherStartChar = nodeAssignments[i].range[0];
if (
otherStartChar < currStartChar &&
otherStartChar > nextStartChar &&
!visited.has(otherStartChar)
) {
nextStartChar = otherStartChar;
assignment = new Assignment(nodeAssignments[i].range, nodeAssignments[i].node);
}
}
visited.add(nextStartChar);
return assignment;
}
One Final Summary of Steps 1–5
Step 1: Detect declaration / assignment expression nodes.
— Populate the data structure identifierAssignments where we map each identifier to an array of all it’s assigned node values.
— Related functions: handleFunctionDeclaration, handleVariableDeclaration,handleImportDeclaration
Step 2: Collect UI Identifiers (Those that we want to validate for)
— Populate UINodes so we can traverse this list of nodes to validate in step 5.
— Related functions: handleUIIdentifier
Step 3: Handle Literals (hardcoded strings)
— Populate invalidNodes where we add all the literals’ ancestor nodes.
— Need to be careful of where the literals are positioned.
This will affect which of the literal’s ancestor nodes should be added to invalidNodes.
— Related functions: handleInvalidLiteral, handleInvalidLiteralInReturn
Step 4: Handle Identifiers (variables/function names)
— Populate translatedNodes and nodesContainsIdentifier.
— Check if identifiers are in the list of allowable gettext strings. If yes, add it’s ancestor nodes into translatedNodes.
— Need to be careful of where the identifiers are positioned and whether they are part of a binary expression. This will affect which ancestor nodes should be added to translatedNodes.
— Related function: markAncestorsTranslated
— Otherwise, go on to the step of populating nodesContainsIdentifier to match all the identifier’s ancestor nodes to the current identifier.
-> so that later on, we know continue the validation search on these identifiers.
— Related functions: handleIdentifier, handleIdentifiersInFunctionBody, handleIdentifiersInReturn
Note that Steps 1–4 can be carried out in any order and are independent of each other. However, Step 5 has to be the last to run.
Step 5: Start evaluation
— Related functions: evaluateNodes, validateUINode, obtainNearestNode
— We will only call this function when all our data structures have been populated with all the values of the program.
— evaluateNodes simply iterates through UINodes and calls validateUINode method.
Depending on the exit error and node type, errors will be raised.
— To validate a UI node, we will need to search for it’s latest assignment value and determine if that value is valid.
We do this by doing a DFS search using a stack. From the UI node, we keep “traversing upwards” to all of the nodes
with the same identifier name as the node we are validating for.
— To know which node to traverse next, for each node we are validating for, we call obtainNearestNode to get the latest
node right above it. Then, we check if the node is in invalidNodes or translatedNodes. This can tell us definitively
whether the UI node has a valid or invalid value. If it is not found in either, we will go on to retrieve all the identifiers
associated with the nearest node and repeat the validation for all those.
The full code
// helper.ts
import { TSESTree } from '@typescript-eslint/experimental-utils';
export type DeclarationNode =
| TSESTree.AssignmentExpression
| TSESTree.VariableDeclarator
| TSESTree.FunctionDeclaration
| TSESTree.ImportDeclaration;// Self defined class
import { TSESTree } from '@typescript-eslint/experimental-utils';
import { DeclarationNode } from './astNodes';
export class Assignment {
public range: TSESTree.Range;
public node: DeclarationNode;
constructor(range: TSESTree.Range, node: DeclarationNode) {
this.range = range;
this.node = node;
}
}
export class StackNode {
public identifierName: string;
public currStartChar: number;
constructor(identifierName: string, currStartChar: number) {
this.identifierName = identifierName;
this.currStartChar = currStartChar;
}
}
export class Identifier {
public identifier: string;
public range: TSESTree.Range;
constructor(identifier: string, range: TSESTree.Range) {
this.identifier = identifier;
this.range = range;
}
}
import { TSESTree } from '@typescript-eslint/experimental-utils';
import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint/Rule';
import { createRule, ignoreFiles } from '../util/rule';
import { gettextFunctionNames } from '../constants';
import {
isIdentifierOnLHSOfAssignOrDeclare,
isNodeBinaryExpression,
isNodeFunction,
isNodeFunctionDeclaration,
} from '../util/ast';
import { DeclarationNode } from '../types/astNodes';
import { Assignment, Identifier, StackNode } from '../types/helper';
const ruleDocumentationUrl = '';
const messages = {
variableNeedI18n:
'i18n: Variables used in the UI should be wrapped with gettext functions',
functionNeedI18n:
'i18n: Functions used in the UI should be wrapped with gettext functions',
};
const rule = createRule(ruleDocumentationUrl)({
name: 'no-hardcoded-ui-variable-function',
meta: {
type: 'suggestion',
docs: {
description:
'UI variables or function calls should be wrapped in i18n gettext functions',
category: 'Best Practices',
recommended: 'warn',
},
schema: [],
messages,
},
defaultOptions: [],
create<TOptions extends readonly unknown[], TMessageIds extends string>(
context: Readonly<RuleContext<TMessageIds, TOptions>>
) {
// Store all the validated nodes with proper gettext
const translatedNodes = new Set<TSESTree.Node>();
// Store all the invalid imports nodes: External Library import
const invalidNodes = new Set<TSESTree.Node>();
// Store all the variable/function UI nodes
const UINodes: TSESTree.Identifier[] = [];
// Maps a node to all the identifiers that it contains
const nodeContainsIdentifiers: Map<TSESTree.Node, Set<Identifier>> = new Map();
// Maps an identifier to all the values that it has been assigned to in the code
const identifierAssignments: Map<string, Array<Assignment>> = new Map();
// Collect all UI identifiers for validation
function handleUIIdentifier(node: TSESTree.Identifier): void {
UINodes.push(node);
}
function handleIdentifiersNotInFunctionBody(node: TSESTree.Identifier): void {
const { name } = node;
const ancestors = context.getAncestors();
// Only handle Identifiers not enclosed within a function body
const nodeHasFunctionAncestor = ancestors.filter((anc) => isNodeFunction(anc));
if (nodeHasFunctionAncestor.length > 0) return;
if (gettextFunctionNames.has(name)) {
applyUntilBinaryExprAncestors(function(anc: TSESTree.Node) {
translatedNodes.add(anc);
});
return;
}
// Skip if the identifier is found on the LHS of an assignment / declarator
// We only want to associate the ancestor nodes
// with identifiers found on the RHS of an assignment / declarator
if (!isIdentifierOnLHSOfAssignOrDeclare(node)) {
// Associate current identifier with all of its' ancestor node
applyToAllAncestors(function(anc: TSESTree.Node) {
setNodeContainsIdentifiers(name, node, anc);
});
}
}
// Handle identifiers enclosed within a function body with more than
// 1 statement in the body
// Excludes identifiers inside ReturnStatement
function handleIdentifiersInFunctionBody(node: TSESTree.Identifier): void {
const { name } = node;
if (gettextFunctionNames.has(name)) {
// Only mark ancestors in the same line as translated
// (1) Ignore ancestor function nodes
// &
// (2) ancestor nodes if identifier is part of a binary expression
// If the current expression that the identifier (eg, a) is part of belongs to a binary expression
// Eg, expr = a + b, we will not be able to mark for sure that it's ancestors are translated as
// it's validity also depends on the other identifier (eg, b) / item in the expression
applyUntilBinaryOrFunctionAncestors(function(anc: TSESTree.Node) {
translatedNodes.add(anc);
});
return;
}
// Associate current identifier with each of it's ancestor node except for function ancestors
// and beyond
applyUntilFunctionAncestors(function(anc: TSESTree.Node) {
setNodeContainsIdentifiers(name, node, anc);
});
}
// Handle identifiers which is either the sole statement in the function body
// or is located in the ReturnStatement
function handleIdentifiersInReturn(node: TSESTree.Identifier): void {
const { name } = node;
if (gettextFunctionNames.has(name)) {
applyUntilBinaryExprAncestors(function(anc: TSESTree.Node) {
translatedNodes.add(anc);
});
return;
}
applyToAllAncestors(function(anc: TSESTree.Node) {
// Associate current identifier with all of its' ancestor node
setNodeContainsIdentifiers(name, node, anc);
});
}
function evaluateNodes(): void {
UINodes.forEach((node) => {
// Reset visited nodes for a brand new validation path
const exitError = validateUINode(node);
if (exitError) {
if (isIdentifierPartOfFunctionCall(node)) {
context.report({
node,
messageId: 'functionNeedI18n' as TMessageIds,
});
return;
}
// Variable reference
context.report({
node,
messageId: 'variableNeedI18n' as TMessageIds,
});
}
});
}
function validateUINode(node: TSESTree.Identifier): boolean {
// Tracks the range of the nodes we have visited during validation to prevent infinite loop
// Example: In the case that we have stmt such as:
// (Line 1) let a = gettext("good");
// (Line 2) a = a + "string"
// we will not want to fall into the loop of visiting the same node for a at Line 2 repeatedly
const visited: Set<number> = new Set();
const stack: [StackNode] = [new StackNode(node.name, node.range[0])];
while (stack.length > 0) {
console.log('stack: ', stack);
const currNode = stack.pop();
if (currNode === undefined) continue;
const nearestNodeInfo = obtainNearestNode(currNode, visited);
if (nearestNodeInfo === undefined) {
// Could be the case where the code has visited and hence "validated" the node before and there
// is no need to repeat the work or that the currNode has no identifiers associated with it and
// hence has no violations.
console.log('No next node retrieved for', currNode.identifierName);
continue;
}
if (invalidNodes.has(nearestNodeInfo.node)) {
console.log('Nearest node is an invalid node');
return true;
}
if (translatedNodes.has(nearestNodeInfo.node)) {
console.log('Found nearest node in Translated');
continue;
}
// Obtain the identifiers belonging to that node we are going to visit next
if (nodeContainsIdentifiers.has(nearestNodeInfo.node)) {
const nextIdentifierNamesSet = nodeContainsIdentifiers.get(
nearestNodeInfo.node
);
console.log(
'Node is associated with the following identifiers: ',
nextIdentifierNamesSet
);
// @ts-ignore
nextIdentifierNamesSet.forEach((next) => {
stack.push(new StackNode(next.identifier, next.range[0]));
});
}
}
return false;
}
// Obtain the latest node assignment for the given node
function obtainNearestNode(
currNode: StackNode,
visited: Set<number>
): Assignment | undefined {
const identifierName = currNode.identifierName;
const currStartChar = currNode.currStartChar;
// Get all the Nodes that the identifier has been assigned to
const nodeAssignments: Array<Assignment> | undefined = identifierAssignments.get(
identifierName
);
// Identifier not assigned any value
if (nodeAssignments === undefined) return undefined;
// Identifier is the identifier of a Function declaration
// There is only 1 Function declaration for each identifier and this declaration could be found anywhere in
// the code. Hence, we can directly return the node found without checking for it's range value.
if (nodeAssignments.length === 1 && isNodeFunctionDeclaration(nodeAssignments[0].node))
return new Assignment(nodeAssignments[0].range, nodeAssignments[0].node);
let nextStartChar = 0;
let assignment;
for (let i = 0; i < nodeAssignments.length; i++) {
const otherStartChar = nodeAssignments[i].range[0];
if (
otherStartChar < currStartChar &&
otherStartChar > nextStartChar &&
!visited.has(otherStartChar)
) {
nextStartChar = otherStartChar;
assignment = new Assignment(nodeAssignments[i].range, nodeAssignments[i].node);
}
}
visited.add(nextStartChar);
return assignment;
}
function handleAssignmentExpression(node: TSESTree.AssignmentExpression): void {
if (node.left.type === 'Identifier') {
const identifier = node.left.name;
const rangeIdentifier = node.left.range;
// Store the pairing of identifier with it's assigned value
setIdentifierAssignments(identifier, rangeIdentifier, node);
}
}
function handleFunctionDeclaration(node: TSESTree.FunctionDeclaration): void {
if (node.id !== null) {
const identifier = node.id.name;
const rangeIdentifier = node.id.range;
// Store the pairing of identifier with it's assigned value
setIdentifierAssignments(identifier, rangeIdentifier, node);
}
}
function handleVariableDeclaration(node: TSESTree.VariableDeclaration): void {
// @ts-ignore
const identifier = node.declarations[0].id.name;
const rangeIdentifier = node.declarations[0].id.range;
const nodeDeclarator = node.declarations[0];
// Store the pairing of identifier with it's assigned value
setIdentifierAssignments(identifier, rangeIdentifier, nodeDeclarator);
}
function handleImportDeclaration(node: TSESTree.ImportDeclaration): void {
const rangeIdentifier = node.range;
// Mark all imports as safe
node.specifiers.forEach((importSpecifier) => {
const name = importSpecifier.local.name;
translatedNodes.add(node);
setNodeContainsIdentifiers(name, node, node);
setIdentifierAssignments(name, rangeIdentifier, node);
});
}
function handleInvalidLiteral(node: TSESTree.Literal): void {
invalidNodes.add(node);
// Only mark ancestors until function level as invalid
applyUntilFunctionAncestors(function(anc: TSESTree.Node) {
invalidNodes.add(anc);
});
}
function handleInvalidLiteralInReturn(node: TSESTree.Literal): void {
invalidNodes.add(node);
// Mark all ancestors as invalid
applyToAllAncestors(function(anc: TSESTree.Node) {
invalidNodes.add(anc);
});
}
/*
* Helper methods
*/
function setNodeContainsIdentifiers(
name: string,
node: TSESTree.Node,
anc: TSESTree.Node
): void {
// Associate ancestor nodes with the identifier name and range of the node(s) it contains
if (nodeContainsIdentifiers.has(anc)) {
nodeContainsIdentifiers.set(
anc,
// @ts-ignore
nodeContainsIdentifiers.get(anc).add(new Identifier(name, node.range))
);
} else {
const newSet: Set<Identifier> = new Set();
newSet.add(new Identifier(name, node.range));
nodeContainsIdentifiers.set(anc, newSet);
}
}
function setIdentifierAssignments(
name: string,
rangeIdentifier: TSESTree.Range,
node: DeclarationNode
): void {
if (gettextFunctionNames.has(name)) {
return;
}
if (identifierAssignments.has(name)) {
const oldList = identifierAssignments.get(name);
// @ts-ignore
const newList = oldList.concat(new Assignment(rangeIdentifier, node));
identifierAssignments.set(name, newList);
} else {
identifierAssignments.set(name, new Array(new Assignment(rangeIdentifier, node)));
}
}
function applyToAllAncestors(fn: Function): void {
const ancestors = context.getAncestors();
ancestors.forEach((ancestorNode) => {
fn(ancestorNode);
});
}
function applyUntilFunctionAncestors(fn: Function): void {
const ancestors = context.getAncestors();
while (ancestors.length > 0) {
const parent = ancestors.pop();
if (!parent) continue;
if (isNodeFunction(parent)) break;
fn(parent);
}
}
function applyUntilBinaryExprAncestors(fn: Function): void {
const ancestors = context.getAncestors();
while (ancestors.length > 0) {
const parent = ancestors.pop();
if (!parent) continue;
if (isNodeBinaryExpression(parent)) break;
fn(parent);
}
}
function applyUntilBinaryOrFunctionAncestors(fn: Function): void {
const ancestorsOne = context.getAncestors();
while (ancestorsOne.length > 0) {
const parent = ancestorsOne.pop();
if (!parent) continue;
if (isNodeBinaryExpression(parent) || isNodeFunction(parent)) break;
fn(parent);
}
}
return ignoreFiles(context, {
// Examples:
// function a() {}
// Handle function declaration
FunctionDeclaration: handleFunctionDeclaration,
// Examples:
// const variable = "";
// let variable = () => {}
// To detect variables declared
VariableDeclaration: handleVariableDeclaration,
// Detect imports
// Example: import { getUIString, hello } from "someLibrary";
ImportDeclaration: handleImportDeclaration,
// Examples:
// variableDeclaredBefore = "";
// variableDeclaredBefore = () => {}
// To detect assignments that are not declarations
'ExpressionStatement > AssignmentExpression': handleAssignmentExpression,
// Function in JSX expression
// Example: <div>{ getSomeUIString() }</div>
':matches(JSXElement, JSXFragment) > JSXExpressionContainer CallExpression > Identifier': handleUIIdentifier,
// Variable in JSX expression
// Example: <div>{ someVarDefinedAbove }</div>
':matches(JSXElement, JSXFragment) > JSXExpressionContainer > Identifier': handleUIIdentifier,
// Hardcoded literals excluding those inside gettext or import libraries or JSX attribute
// Example of excluded texts:
// gettext("hello"), import { getUIString, hello } from "someLibrary";, <div atrr="hh">{a}<div>
// ':not(CallExpression, ImportDeclaration, JSXAttribute) > Literal':
// Literal can be within a function scope or outside
':not(CallExpression, ImportDeclaration, JSXAttribute) > Literal': handleInvalidLiteral,
// Hardcoded literals (together with other addition) inside ReturnStatement of a function
// function b () {return "s" + a;} const b = () => {return "a"+a;}
'ReturnStatement :not(CallExpression, ImportDeclaration, JSXAttribute) Literal': handleInvalidLiteralInReturn,
// Hardcoded literals (Just 1) inside ReturnStatement of a function
// Example: const b = () => {return "s";} function b () {return "s";}
'ReturnStatement > Literal': handleInvalidLiteralInReturn,
// Hardcoded literals inside a function with only 1 stmt
// Example: const c = () => (b() + "bad string");
// We treat them the same as if they were inside a ReturnStatement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BinaryExpression > Literal': handleInvalidLiteralInReturn,
// Hardcoded literals inside a function with only 1 stmt
// Example: const c = () => ("bad string");
// We treat them the same as if they were inside a ReturnStatement
':matches(ArrowFunctionExpression, FunctionDeclaration) > Literal': handleInvalidLiteralInReturn,
// Function calls, referencing a variable
// Example: <div>{ identifierVar }</div>, identifierFnA(), someObj.identifierFnB()
Identifier: handleIdentifiersNotInFunctionBody,
// Identifier (Not inside ReturnStatement) inside a function body with more than 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BlockStatement[body.length>1] Identifier': handleIdentifiersInFunctionBody,
// Identifier inside return value of function body containing more than 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BlockStatement[body.length>1] ReturnStatement Identifier': handleIdentifiersInReturn,
// Identifier inside return value of a function body containing only 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > BlockStatement[body.length=1] ReturnStatement Identifier': handleIdentifiersInReturn,
// Identifier inside a function body containing only 1 statement
':matches(ArrowFunctionExpression, FunctionDeclaration) > :not(BlockStatement) Identifier': handleIdentifiersInReturn,
'Program:exit': evaluateNodes,
});
},
});
export = rule;
// util/ast.ts helper classfunction isIdentifierOnLHSOfAssignOrDeclare(node: TSESTree.Node): boolean {
if (!node.parent) return false;
return (
(node.parent.type === 'VariableDeclarator' && node.parent.id === node) ||
(node.parent.type === 'AssignmentExpression' && node.parent.left === node) ||
(node.parent.type === 'FunctionDeclaration' && node.parent.id === node)
);
}
function isNodeFunction(node: TSESTree.Node): boolean {
return node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression';
}
function isNodeFunctionDeclaration(node: TSESTree.Node): boolean {
return node.type === 'FunctionDeclaration';
}
function isNodeBinaryExpression(node: TSESTree.Node): boolean {
return node.type === 'BinaryExpression';
}
// TEST CLASS
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
import { ParserOptions } from '@typescript-eslint/experimental-utils/dist/ts-eslint';
import rule from '../../../src/rules/no-hardcoded-ui-variable-function';
const RuleTester = ESLintUtils.RuleTester;
const parserOptions: Readonly<ParserOptions> = {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
};
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions,
});
ruleTester.run('no-hardcoded-ui-variable-function', rule, {
valid: [
// Basic Function call
`function getUIString() {
return gt.gettext('My UI string');
}
return (
<div> { getUIString() }</div>
);`,
`const getUIString = () => gt.gettext('My UI string');
return (
<div> { getUIString() }</div>
);`,
`const getUIString = () => {
return gt.gettext('My UI string');
};
return (
<div> { getUIString() }</div>
);`,
// Interlacing function calls
`function c() {
return gt.gettext('good string');
}
const b = () => c();
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
// Functions placed below the caller
`
function c() {
return (<div> { a() }</div>);
}
function a() {
return gettext(d());
}
function d () {
return "bad string";
}
`,
// Function calls with unrelated lines between
`
const a = () => {
const hello = "hello";
let c = "f";
return gettext(hello);
}
return (
<div> { a() }</div>
);`,
// Variable assignment
`const myText = gt.gettext('My UI string');
const myText1 = gt.gettext('My UI string 1');
return (
<><p>{ myText }</p><div> { myText1 }</div></>
);`,
`let SomeComponent = <div>{ gt.gettext("Some Text") }</div>;
return (
<><div> { SomeComponent }</div></>
);`,
// Interlacing variable assignments with const/let
`const myText = gt.gettext('My UI string');
const myText1 = myText;
return (
<><div> { myText1 }</div></>
);`,
`let myText = gt.gettext('My UI string');
let myText1 = myText;
return (
<><div> { myText1 }</div></>
);`,
// Imported functions
`import { getUIString } from "@company/someInternalLibrary";
return (
<div> { getUIString() }</div>
);`,
`import { someImport } from "./someOtherFile";
const b = () => someImport();
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
`import someDefaultImport from "./someOtherFile";
const b = () => someDefaultImport();
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
`
import { getUIString } from "someLibrary";
return (
<div> { getUIString() }</div>
);`,
`
import someDefaultImport from "someLibrary";
const b = () => someDefaultImport();
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
// Imported variables
`import { someUIString } from "./someOtherFile";
return (
<div> { someUIString }</div>
);`,
`import { someImportedVar } from "./someLibrary";
const b = someImportedVar;
const c = b;
const someUIString = c;
return (
<div> { someUIString }</div>
);`,
`
import { someUIString } from "someLibrary";
return (
<div> { someUIString }</div>
);`,
`
import { someImportedVar } from "someLibrary";
const b = someImportedVar;
const c = b;
const someUIString = c;
return (
<div> { someUIString }</div>
);`,
// "let" variables with multiple reassignments
`let coworkersHistory = '';
if (byCoworkerDate && byCoworkerEmail) {
coworkersHistory = i18n.gettext('Contacted: {0} by {1}', [byCoworkerDate, byCoworkerEmail]);
}
return (
<div className="rezemp-ContactedLabel">
<span className="rezemp-ContactedLabel-byCoworker" data-cauto-id="contacted-by-coworker">{coworkersHistory}</span>
</div>
);
`,
`let coworkersHistory = i18n.gettext('Contacted: {0} by {1}', [byCoworkerDate, byCoworkerEmail]);
coworkersHistory = "bad string";
coworkersHistory = gettext("good string");
return (
<div className="rezemp-ContactedLabel">
<span className="rezemp-ContactedLabel-byCoworker" data-cauto-id="contacted-by-coworker">{coworkersHistory}</span>
</div>
);`,
// "let" functions with multiple reassignments
`
let a = "bad string";
let b = () => gettext("hi");
a = () => {
return b();
}
return (<div>{a()}</div>);
`,
`
let string = "bad string";
string = gettext("hi");
function b() {
return string;
}
function a() {
return b();
}
return (<div>{a()}</div>);
`,
` let d = "bad string";
const b = () => {
let a = "s";
d = gettext("p");
return d;
}
return (<div>{b()}</div>)
`,
// Functions With multiple concatenated return values
// Eg, return a = a + "something"
`
function c() {
let a = gettext("Morning");
let b = gettext("hello");
const d = gettext("bye");
return a + b + d;
}
return (<div>{c()}</div>);
`,
`
let text = "text";
function a () {
newText = gettext(text);
return newText;
}
let b = () => {
return a();
}
b = () => {
const another = gettext("good");
return b() + another;
}
return (
<div> { b() }</div>
);
`,
`const b = () => {
const a = gettext("I am good");
const c = gt.gettext("good string");
return a + c;
};
function a() {
return b();
}
return (
<div>
<div> { a() }</div>
<div> { b() } </div>
</div>
);`,
// Exclude test/storybook/mock files
{
code: `
function getUIString() {
return 'My UI string';
}
return (
<div> { getUIString() }</div>
);`,
filename: 'SomeComponent.test.tsx',
},
{
code: `
function getUIString() {
return 'My UI string';
}
return (
<div> { getUIString() }</div>
);`,
filename: 'SomeComponent.stories.tsx',
},
{
code: `
function getUIString() {
return 'My UI string';
}
return (
<div> { getUIString() }</div>
);`,
filename: '__tests__/SomeComponent.tsx',
},
{
code: `
function getUIString() {
return 'My UI string';
}
return (
<div> { getUIString() }</div>
);`,
filename: '__mocks__/SomeComponent.tsx',
},
// Avoid JSXExpressionContainers that do not make up part of the UI
`
const handleClick = () => null;
return (
<div onClick={ handleClick } />
);
`,
`
const handleClickGenerator = () => () => null;
return (
<div onClick={ handleClickGenerator() } />
);
`,
// Test with functional component without gettext and invalid strings
`const b = () => {
return <Button></Button>;
};
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
// Component props
`const { children, playground } = this.props;`,
],
invalid: [
// Function call
{
code: `
function getUIString() {
return 'My UI string';
}
return (
<div> { getUIString() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
filename: 'ProperComponentNotATest.tsx',
},
{
code: `
function c() {
return 'bad string';
}
const b = () => c();
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
// Function declarations placed below the caller
{
code: `
function c() {
return (<div> { a() }</div>);
}
function a() {
return d();
}
function d() {
const str = "bad string";
return str + gettext(str);
}
`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
// Functions With multiple concatenated return values
// Eg, a = a + "some value";
{
code: `
function c() {
let a = "hi";
let b = gettext("hello");
const d = gettext("bye");
return a + b + d;
}
return (<div>{c()}</div>);
`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
{
code: `
const b = () => {
const a = gettext("I am good");
const c = "bad string";
return a + c;
};
function a() {
return b();
}
return (
<div> { a() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
{
code: `
const a = () => {
let b = "bad string";
const c = gettext("string") + b;
return b + c;
}
return (
<div> { a() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
{
code: `
function b () {
const a = gettext("I am good");
return a + "bad string";
};
function d() {
return b();
}
return (
<div> { d() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
{
code: `
const b = () => {
return gettext("good string");
};
const c = () => (b() + "bad string");
return (
<div> { c() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
{
code: `
let b = () => ("bad string");
b = b + gettext("hi");
return (
<div> { b() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
{
code: `
function a () {
const text = "hello";
return text;
}
let b = () => {
return a();
}
b = () => {
const another = gettext("good");
return b() + another;
}
return (
<div> { a() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
// Functions with multiple reassignments
{
code: `
let a = () => (gettext("good string"));
a = () => {
return "bad string";
}
return (
<div> { a() }</div>
);`,
errors: [
{
messageId: 'functionNeedI18n',
},
],
},
// Variable assignment
{
code: `
const myText = 'My UI string';
const myText1 = gt.gettext('My UI string 1');
return (
<><p>{ myText }</p><div> { myText1 }</div></>
);`,
errors: [
{
messageId: 'variableNeedI18n',
},
],
},
{
code: `
const myText = 'My UI string';
const myText1 = myText;
return (
<><div> { myText1 }</div></>
);`,
errors: [
{
messageId: 'variableNeedI18n',
},
],
},
// Variables with multiple assignments
{
code: `
let a = "hi";
let myText = gettext('My UI string');
myText = myText + a;
return (
<><div> { myText }</div></>
);`,
errors: [
{
messageId: 'variableNeedI18n',
},
],
},
{
code: `
let a = "hi";
a = gt.gettext(a);
let myText = gettext('My UI string');
myText = myText + 'is now complete';
return (
<><div> { myText }</div></>
);`,
errors: [
{
messageId: 'variableNeedI18n',
},
],
},
],
});
The end! It has been tiring writing this…but it also enabled me to think about my logic for this task and spot some edge cases that I missed out previously….However, I feel that the entire code is very lengthy…but not sure how to do it better????