import { dirname, isAbsolute, relative, sep } from 'path'; import * as BabelCore from '@babel/core'; import * as BabelTypes from '@babel/types'; import { CommentHint, getCommentHintForPath } from '../comments'; import { ExtractedKey } from '../keys'; /** * Error thrown in case extraction of a node failed. */ export class ExtractionError extends Error { nodePath: BabelCore.NodePath; constructor(message: string, node: BabelCore.NodePath) { super(message); this.nodePath = node; } } /** * Given a value, if the value is an array, return the first * item of the array. Otherwise, return the value. * * This is mainly useful to parse namespaces which can be strings * as well as array of strings. */ export function getFirstOrNull(val: T | null | T[]): T | null { if (Array.isArray(val)) val = val[0]; return val === undefined ? null : val; } /** * Given comment hints and a path, infer every I18NextOption we can from the comment hints. * @param path path on which the comment hints should apply * @param commentHints parsed comment hints * @returns every parsed option that could be infered. */ export function parseI18NextOptionsFromCommentHints( path: BabelCore.NodePath, commentHints: CommentHint[], ): Partial { const nsCommentHint = getCommentHintForPath(path, 'NAMESPACE', commentHints); const contextCommentHint = getCommentHintForPath( path, 'CONTEXT', commentHints, ); const pluralCommentHint = getCommentHintForPath( path, 'PLURAL', commentHints, ); const res: Partial = {}; if (nsCommentHint !== null) { res.ns = nsCommentHint.value; } if (contextCommentHint !== null) { if (['', 'enable'].includes(contextCommentHint.value)) { res.contexts = true; } else if (contextCommentHint.value === 'disable') { res.contexts = false; } else { try { const val = JSON.parse(contextCommentHint.value); if (Array.isArray(val)) res.contexts = val; else res.contexts = [contextCommentHint.value]; } catch (err) { res.contexts = [contextCommentHint.value]; } } } if (pluralCommentHint !== null) { if (pluralCommentHint.value === 'disable') { res.hasCount = false; } else { res.hasCount = true; } } return res; } /** * Improved version of BabelCore `referencesImport` function that also tries to detect wildcard * imports. */ export function referencesImport( nodePath: BabelCore.NodePath, moduleSource: string, importName: string, ): boolean { if (nodePath.referencesImport(moduleSource, importName)) return true; if (nodePath.isMemberExpression() || nodePath.isJSXMemberExpression()) { const obj = nodePath.get('object'); const prop = nodePath.get('property'); if ( Array.isArray(obj) || Array.isArray(prop) || (!prop.isIdentifier() && !prop.isJSXIdentifier()) ) return false; return ( obj.referencesImport(moduleSource, '*') && prop.node.name === importName ); } return false; } /** * Whether a class-instance function call expression matches a known method * @param nodePath: node path to evaluate * @param parentNames: list for any class-instance names to match * @param childName specific function from parent module to match */ export function referencesChildIdentifier( nodePath: BabelCore.NodePath, parentNames: string[], childName: string, ): boolean { if (!nodePath.isMemberExpression()) return false; const obj = nodePath.get('object'); if (!obj.isIdentifier()) return false; const prop = nodePath.get('property'); if (Array.isArray(prop) || !prop.isIdentifier()) return false; return parentNames.includes(obj.node.name) && prop.node.name === childName; } /** * Evaluates a node path if it can be evaluated with confidence. * * @param path: node path to evaluate * @returns null if the node path couldn't be evaluated */ export function evaluateIfConfident( // eslint-disable-next-line @typescript-eslint/no-explicit-any path?: BabelCore.NodePath | null, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { if (!path || !path.node) { return null; } const evaluation = path.evaluate(); if (evaluation.confident) { return evaluation.value; } return null; } /** * Generator that iterates on all keys in an object expression. * @param path the node path of the object expression * @param key the key to find in the object expression. * @yields [evaluated key, node path of the object expression property] */ export function* iterateObjectExpression( path: BabelCore.NodePath, ): IterableIterator< [string, BabelCore.NodePath] > { const properties = path.get('properties'); for (const prop of properties) { const keyPath = prop.get('key'); if (Array.isArray(keyPath)) continue; let keyEvaluation = null; if (keyPath.isLiteral()) { keyEvaluation = evaluateIfConfident(keyPath); } else if (keyPath.isIdentifier()) { keyEvaluation = keyPath.node.name; } else { continue; } yield [keyEvaluation, prop]; } } /** * Try to find a key in an object expression. * @param path the node path of the object expression * @param key the key to find in the object expression. * @returns the corresponding node or null if it wasn't found */ export function findKeyInObjectExpression( path: BabelCore.NodePath, key: string, ): BabelCore.NodePath | null { for (const [keyEvaluation, prop] of iterateObjectExpression(path)) { if (keyEvaluation === key) return prop; } return null; } /** * Find a JSX attribute given its name. * @param path path of the jsx attribute * @param name name of the attribute to look for * @return The JSX attribute corresponding to the given name, or null if no * attribute with this name could be found. */ export function findJSXAttributeByName( path: BabelCore.NodePath, name: string, ): BabelCore.NodePath | null { const openingElement = path.get('openingElement'); const attributes = openingElement.get('attributes'); for (const attribute of attributes) { if (!attribute.isJSXAttribute()) continue; const attributeName = attribute.get('name'); if (!attributeName.isJSXIdentifier()) continue; if (name === attributeName.node.name) return attribute; } return null; } /** * Attempt to find the latest assigned value for a given identifier. * * For instance, given the following code: * const foo = 'bar'; * console.log(foo); * * resolveIdentifier(fooNodePath) should return the 'bar' literal. * * Obviously, this will only work in quite simple cases. * * @param nodePath: node path to resolve * @return the resolved expression or null if it could not be resolved. */ export function resolveIdentifier( nodePath: BabelCore.NodePath, ): BabelCore.NodePath | null { const bindings = nodePath.scope.bindings[nodePath.node.name]; if (!bindings) return null; const declarationExpressions = [ ...(bindings.path.isVariableDeclarator() ? [bindings.path.get('init')] : []), ...bindings.constantViolations .filter((p) => p.isAssignmentExpression()) .map((p) => p.get('right')), ]; if (declarationExpressions.length === 0) return null; const latestDeclarator = declarationExpressions[declarationExpressions.length - 1]; if (Array.isArray(latestDeclarator)) return null; if (!latestDeclarator.isExpression()) return null; return latestDeclarator; } /** * Check whether a given node is a custom import. * * @param absoluteNodePaths: list of possible custom nodes, with their source * modules and import names. * @param path: node path to check * @param name: node name to check * @returns true if the given node is a match. */ export function isCustomImportedNode( absoluteNodePaths: readonly [string, string][], path: BabelCore.NodePath, name: BabelCore.NodePath, ): boolean { return absoluteNodePaths.some(([sourceModule, importName]): boolean => { if (isAbsolute(sourceModule)) { let relativeSourceModulePath = relative( dirname(path.state.filename), sourceModule, ); if (!relativeSourceModulePath.startsWith('.')) { relativeSourceModulePath = '.' + sep + relativeSourceModulePath; } // Absolute path to the source module, let's try a relative path first. if (referencesImport(name, relativeSourceModulePath, importName)) { return true; } } return referencesImport(name, sourceModule, importName); }); }