eslint-plugin-intlayer 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +310 -0
  2. package/dist/cjs/index.cjs +105 -0
  3. package/dist/cjs/index.cjs.map +1 -0
  4. package/dist/cjs/rules/enforceAdapterImport.cjs +81 -0
  5. package/dist/cjs/rules/enforceAdapterImport.cjs.map +1 -0
  6. package/dist/cjs/rules/noDynamicFieldAccess.cjs +130 -0
  7. package/dist/cjs/rules/noDynamicFieldAccess.cjs.map +1 -0
  8. package/dist/cjs/rules/noRawText.cjs +176 -0
  9. package/dist/cjs/rules/noRawText.cjs.map +1 -0
  10. package/dist/cjs/rules/staticDictionaryKey.cjs +89 -0
  11. package/dist/cjs/rules/staticDictionaryKey.cjs.map +1 -0
  12. package/dist/cjs/utils/ast.cjs +151 -0
  13. package/dist/cjs/utils/ast.cjs.map +1 -0
  14. package/dist/cjs/utils/callerMatcher.cjs +130 -0
  15. package/dist/cjs/utils/callerMatcher.cjs.map +1 -0
  16. package/dist/cjs/utils/createRule.cjs +26 -0
  17. package/dist/cjs/utils/createRule.cjs.map +1 -0
  18. package/dist/cjs/utils/templates.cjs +148 -0
  19. package/dist/cjs/utils/templates.cjs.map +1 -0
  20. package/dist/esm/index.mjs +101 -0
  21. package/dist/esm/index.mjs.map +1 -0
  22. package/dist/esm/rules/enforceAdapterImport.mjs +80 -0
  23. package/dist/esm/rules/enforceAdapterImport.mjs.map +1 -0
  24. package/dist/esm/rules/noDynamicFieldAccess.mjs +129 -0
  25. package/dist/esm/rules/noDynamicFieldAccess.mjs.map +1 -0
  26. package/dist/esm/rules/noRawText.mjs +175 -0
  27. package/dist/esm/rules/noRawText.mjs.map +1 -0
  28. package/dist/esm/rules/staticDictionaryKey.mjs +88 -0
  29. package/dist/esm/rules/staticDictionaryKey.mjs.map +1 -0
  30. package/dist/esm/utils/ast.mjs +143 -0
  31. package/dist/esm/utils/ast.mjs.map +1 -0
  32. package/dist/esm/utils/callerMatcher.mjs +126 -0
  33. package/dist/esm/utils/callerMatcher.mjs.map +1 -0
  34. package/dist/esm/utils/createRule.mjs +25 -0
  35. package/dist/esm/utils/createRule.mjs.map +1 -0
  36. package/dist/esm/utils/templates.mjs +145 -0
  37. package/dist/esm/utils/templates.mjs.map +1 -0
  38. package/dist/types/index.d.ts +67 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/rules/enforceAdapterImport.d.ts +6 -0
  41. package/dist/types/rules/enforceAdapterImport.d.ts.map +1 -0
  42. package/dist/types/rules/noDynamicFieldAccess.d.ts +7 -0
  43. package/dist/types/rules/noDynamicFieldAccess.d.ts.map +1 -0
  44. package/dist/types/rules/noRawText.d.ts +25 -0
  45. package/dist/types/rules/noRawText.d.ts.map +1 -0
  46. package/dist/types/rules/staticDictionaryKey.d.ts +7 -0
  47. package/dist/types/rules/staticDictionaryKey.d.ts.map +1 -0
  48. package/dist/types/utils/ast.d.ts +105 -0
  49. package/dist/types/utils/ast.d.ts.map +1 -0
  50. package/dist/types/utils/callerMatcher.d.ts +58 -0
  51. package/dist/types/utils/callerMatcher.d.ts.map +1 -0
  52. package/dist/types/utils/createRule.d.ts +23 -0
  53. package/dist/types/utils/createRule.d.ts.map +1 -0
  54. package/dist/types/utils/templates.d.ts +71 -0
  55. package/dist/types/utils/templates.d.ts.map +1 -0
  56. package/package.json +105 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noDynamicFieldAccess.cjs","names":["getStaticStringValue","createRule","getActiveDescriptors","collectImportSources","toAstNode","matchCaller"],"sources":["../../../src/rules/noDynamicFieldAccess.ts"],"sourcesContent":["import type { Scope } from 'eslint';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport {\n collectImportSources,\n getActiveDescriptors,\n matchCaller,\n} from '../utils/callerMatcher';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'dynamicField' | 'dynamicMessageId';\n\n/**\n * What a variable bound from a caller call holds.\n *\n * - `content` — the dictionary content object, or a destructured subtree of\n * it (`useIntlayer`, `getIntlayer`).\n * - `translator` — a translation function taking a message path\n * (`useTranslations`, the `t` of `useTranslation`).\n */\ntype BindingKind = 'content' | 'translator';\n\n/**\n * Property name holding the translation function in a `destructured-t` result,\n * e.g. `const { t } = useTranslation('home')`.\n */\nconst TRANSLATOR_PROPERTY_NAME = 't';\n\n/**\n * True when a computed member access reads a field the compiler can still name:\n * a literal (`content['title']`, `items[0]`) or an uninterpolated template.\n *\n * @param property - The `property` node of a computed MemberExpression.\n */\nconst isStaticComputedProperty = (property: AstNode | null): boolean => {\n if (!property) return false;\n\n if (property.type === 'Literal') return true;\n\n return getStaticStringValue(property) !== null;\n};\n\n/**\n * The variables a declarator binds, paired with the identifier that declared\n * each one — needed to tell `t` apart from `i18n` in a destructuring pattern.\n *\n * @param variables - Result of `sourceCode.getDeclaredVariables(declarator)`.\n * @param identifier - The declaring identifier to look up.\n */\nconst findVariableForIdentifier = (\n variables: Scope.Variable[],\n identifier: AstNode\n): Scope.Variable | null =>\n variables.find((variable) =>\n variable.identifiers.some(\n (declared) => (declared as unknown as AstNode) === identifier\n )\n ) ?? null;\n\n/**\n * Locate the identifier bound to the `t` property of an object pattern,\n * honouring renames (`const { t: translate } = …`).\n *\n * @param pattern - The declarator's `id`, expected to be an ObjectPattern.\n */\nconst findTranslatorIdentifier = (pattern: AstNode): AstNode | null => {\n if (pattern.type !== 'ObjectPattern') return null;\n\n const properties = (pattern['properties'] as AstNode[] | undefined) ?? [];\n\n for (const property of properties) {\n if (property.type !== 'Property' || property['computed'] === true) continue;\n\n const key = property['key'] as AstNode | undefined;\n const keyName =\n key?.type === 'Identifier'\n ? (key['name'] as string)\n : key?.type === 'Literal'\n ? String(key['value'])\n : null;\n\n if (keyName !== TRANSLATOR_PROPERTY_NAME) continue;\n\n const value = property['value'] as AstNode | undefined;\n\n return value?.type === 'Identifier' ? value : null;\n }\n\n return null;\n};\n\nexport const noDynamicFieldAccess = createRule<[], MessageIds>(\n 'no-dynamic-field-access',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require the field read from an Intlayer dictionary to be statically known',\n },\n messages: {\n dynamicField:\n 'Computed access on Intlayer content is not statically analyzable. The purge pass strips fields it cannot see used, so this may read `undefined` at runtime — read the field by name instead.',\n dynamicMessageId:\n 'The message path passed to `{{caller}}` must be a static string. The purge pass strips fields it cannot see used, so this may read `undefined` at runtime.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n let activeDescriptors = getActiveDescriptors(new Set());\n\n /** Report every computed, non-static member access on a bound variable. */\n const checkContentVariable = (variable: Scope.Variable): void => {\n for (const reference of variable.references) {\n const identifier =\n reference.identifier as unknown as AstNodeWithParent;\n const parent = identifier.parent;\n\n if (parent?.type !== 'MemberExpression') continue;\n\n if (parent['object'] !== identifier) continue;\n\n if (parent['computed'] !== true) continue;\n\n if (isStaticComputedProperty(parent['property'] as AstNode)) continue;\n\n context.report({\n node: parent as never,\n messageId: 'dynamicField',\n });\n }\n };\n\n /** Report every call of a translation function with a runtime message path. */\n const checkTranslatorVariable = (\n variable: Scope.Variable,\n callerName: string\n ): void => {\n for (const reference of variable.references) {\n const identifier =\n reference.identifier as unknown as AstNodeWithParent;\n const parent = identifier.parent;\n\n if (parent?.type !== 'CallExpression') continue;\n\n if (parent['callee'] !== identifier) continue;\n\n const [firstArgument] = (parent['arguments'] as AstNode[]) ?? [];\n\n if (!firstArgument) continue;\n\n if (getStaticStringValue(firstArgument) !== null) continue;\n\n context.report({\n node: firstArgument as never,\n messageId: 'dynamicMessageId',\n data: { caller: callerName },\n });\n }\n };\n\n return {\n Program: (node) => {\n activeDescriptors = getActiveDescriptors(\n collectImportSources(toAstNode(node))\n );\n },\n\n VariableDeclarator: (node) => {\n const astNode = toAstNode(node);\n const init = astNode['init'] as AstNode | undefined;\n\n if (!init) return;\n\n const match = matchCaller(init, activeDescriptors);\n\n if (!match) return;\n\n const { descriptor } = match;\n const shape = descriptor.translationFunction;\n\n const kind: BindingKind | null =\n shape === 'content'\n ? 'content'\n : shape === 'return-value' || shape === 'destructured-t'\n ? 'translator'\n : null;\n\n if (!kind) return;\n\n const declaredVariables = sourceCode.getDeclaredVariables(node);\n const id = astNode['id'] as AstNode;\n\n if (kind === 'content') {\n // Every variable the pattern binds is content (the root object or a\n // destructured subtree of it), so all of them are worth checking.\n for (const variable of declaredVariables) {\n checkContentVariable(variable);\n }\n\n return;\n }\n\n if (shape === 'return-value') {\n if (id.type !== 'Identifier') return;\n\n const variable = findVariableForIdentifier(declaredVariables, id);\n\n if (variable) {\n checkTranslatorVariable(variable, descriptor.callerName);\n }\n\n return;\n }\n\n const translatorIdentifier = findTranslatorIdentifier(id);\n\n if (!translatorIdentifier) return;\n\n const variable = findVariableForIdentifier(\n declaredVariables,\n translatorIdentifier\n );\n\n if (variable) {\n checkTranslatorVariable(variable, descriptor.callerName);\n }\n },\n };\n },\n }\n);\n"],"mappings":"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"}
@@ -0,0 +1,176 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_utils_ast = require('../utils/ast.cjs');
3
+ const require_utils_createRule = require('../utils/createRule.cjs');
4
+ const require_utils_templates = require('../utils/templates.cjs');
5
+ let _intlayer_config_defaultValues = require("@intlayer/config/defaultValues");
6
+ let _intlayer_config_extract = require("@intlayer/config/extract");
7
+
8
+ //#region src/rules/noRawText.ts
9
+ /**
10
+ * Elements whose text is markup, code or metadata rather than user-facing copy.
11
+ * Extracting them would be wrong, so they are skipped by default.
12
+ */
13
+ const DEFAULT_IGNORED_ELEMENTS = [
14
+ "code",
15
+ "pre",
16
+ "script",
17
+ "style"
18
+ ];
19
+ /**
20
+ * True for a content declaration file, where raw strings are the whole point.
21
+ *
22
+ * @param filename - Absolute path of the linted file.
23
+ */
24
+ const isContentDeclarationFile = (filename) => _intlayer_config_defaultValues.FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));
25
+ /**
26
+ * Walk up from a JSX child or attribute to the tag name that encloses it.
27
+ *
28
+ * @param node - The reported node.
29
+ */
30
+ const getEnclosingElementName = (node) => {
31
+ let current = node;
32
+ while (current) {
33
+ if (current.type === "JSXElement") return require_utils_ast.getJsxElementName(current["openingElement"]);
34
+ if (current.type === "JSXOpeningElement") return require_utils_ast.getJsxElementName(current);
35
+ current = current.parent;
36
+ }
37
+ return null;
38
+ };
39
+ /**
40
+ * True when the string literal sits in a position that is structural rather
41
+ * than user-facing: module specifiers, property keys, directives, and the
42
+ * arguments of the Intlayer getters themselves.
43
+ *
44
+ * @param node - The Literal node.
45
+ */
46
+ const isStructuralPosition = (node) => {
47
+ const parent = node.parent;
48
+ if (!parent) return true;
49
+ switch (parent.type) {
50
+ case "ImportDeclaration":
51
+ case "ImportExpression":
52
+ case "ExportNamedDeclaration":
53
+ case "ExportAllDeclaration":
54
+ case "JSXAttribute":
55
+ case "TSLiteralType":
56
+ case "TSEnumMember":
57
+ case "ExpressionStatement": return true;
58
+ case "Property": return parent["key"] === node && parent["computed"] !== true;
59
+ case "MemberExpression": return parent["property"] === node;
60
+ default: return false;
61
+ }
62
+ };
63
+ const noRawText = require_utils_createRule.createRule("no-raw-text", {
64
+ meta: {
65
+ type: "problem",
66
+ docs: { description: "Report user-facing strings that are not declared in an Intlayer dictionary" },
67
+ messages: {
68
+ rawText: "Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.",
69
+ rawAttribute: "Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.",
70
+ rawStringLiteral: "Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file."
71
+ },
72
+ schema: [{
73
+ type: "object",
74
+ properties: {
75
+ attributes: {
76
+ type: "array",
77
+ items: { type: "string" }
78
+ },
79
+ ignoreElements: {
80
+ type: "array",
81
+ items: { type: "string" }
82
+ },
83
+ ignorePatterns: {
84
+ type: "array",
85
+ items: { type: "string" }
86
+ },
87
+ includeStringLiterals: { type: "boolean" }
88
+ },
89
+ additionalProperties: false
90
+ }]
91
+ },
92
+ create: (context) => {
93
+ const filename = context.filename;
94
+ if (isContentDeclarationFile(filename)) return {};
95
+ const [options = {}] = context.options;
96
+ const attributes = new Set(options.attributes ?? _intlayer_config_extract.ATTRIBUTES_TO_EXTRACT);
97
+ const ignoredElements = new Set(options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS);
98
+ const ignorePatterns = (options.ignorePatterns ?? []).map((pattern) => new RegExp(pattern, "u"));
99
+ const includeStringLiterals = options.includeStringLiterals === true;
100
+ /** True when the text is copy the extractor would pick up. */
101
+ const isReportableText = (text) => (0, _intlayer_config_extract.shouldExtract)(text) && !ignorePatterns.some((pattern) => pattern.test(text.trim()));
102
+ /** Collapse whitespace so multi-line JSX text reads well in the report. */
103
+ const formatText = (text) => {
104
+ const normalized = text.replace(/\s+/gu, " ").trim();
105
+ return normalized.length > 40 ? `${normalized.slice(0, 40)}…` : normalized;
106
+ };
107
+ /**
108
+ * Handlers for the markup languages that are not JSX — Vue, Svelte and
109
+ * Angular templates. Reports through the same predicate and options as
110
+ * the JSX path so a project gets identical results whatever it is written
111
+ * in.
112
+ */
113
+ const templateVisitor = require_utils_templates.createTemplateVisitor(({ node, text, attributeName, elementName }) => {
114
+ if (elementName && ignoredElements.has(elementName)) return;
115
+ if (attributeName !== null && !attributes.has(attributeName)) return;
116
+ if (!isReportableText(text)) return;
117
+ context.report({
118
+ node,
119
+ messageId: attributeName === null ? "rawText" : "rawAttribute",
120
+ data: {
121
+ text: formatText(text),
122
+ ...attributeName === null ? {} : { attribute: attributeName }
123
+ }
124
+ });
125
+ });
126
+ return require_utils_templates.combineVisitors(context.sourceCode.parserServices, {
127
+ JSXText: (node) => {
128
+ const astNode = require_utils_ast.toAstNode(node);
129
+ const text = astNode["value"];
130
+ if (!isReportableText(text)) return;
131
+ const elementName = getEnclosingElementName(astNode.parent);
132
+ if (elementName && ignoredElements.has(elementName)) return;
133
+ context.report({
134
+ node,
135
+ messageId: "rawText",
136
+ data: { text: formatText(text) }
137
+ });
138
+ },
139
+ JSXAttribute: (node) => {
140
+ const astNode = require_utils_ast.toAstNode(node);
141
+ const attributeName = require_utils_ast.getJsxAttributeName(astNode);
142
+ if (!attributeName || !attributes.has(attributeName)) return;
143
+ const valueNode = require_utils_ast.getJsxAttributeValueNode(astNode);
144
+ const text = require_utils_ast.getStaticStringValue(valueNode);
145
+ if (text === null || !isReportableText(text)) return;
146
+ const elementName = getEnclosingElementName(astNode.parent);
147
+ if (elementName && ignoredElements.has(elementName)) return;
148
+ context.report({
149
+ node,
150
+ messageId: "rawAttribute",
151
+ data: {
152
+ attribute: attributeName,
153
+ text: formatText(text)
154
+ }
155
+ });
156
+ },
157
+ Literal: (node) => {
158
+ if (!includeStringLiterals) return;
159
+ const astNode = require_utils_ast.toAstNode(node);
160
+ if (typeof astNode["value"] !== "string") return;
161
+ if (isStructuralPosition(astNode)) return;
162
+ const text = astNode["value"];
163
+ if (!isReportableText(text)) return;
164
+ context.report({
165
+ node,
166
+ messageId: "rawStringLiteral",
167
+ data: { text: formatText(text) }
168
+ });
169
+ }
170
+ }, templateVisitor);
171
+ }
172
+ });
173
+
174
+ //#endregion
175
+ exports.noRawText = noRawText;
176
+ //# sourceMappingURL=noRawText.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noRawText.cjs","names":["FILE_EXTENSIONS","getJsxElementName","createRule","ATTRIBUTES_TO_EXTRACT","shouldExtract","createTemplateVisitor","combineVisitors","toAstNode","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/noRawText.ts"],"sourcesContent":["import { FILE_EXTENSIONS } from '@intlayer/config/defaultValues';\nimport { ATTRIBUTES_TO_EXTRACT, shouldExtract } from '@intlayer/config/extract';\nimport {\n type AstNode,\n type AstNodeWithParent,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport { createRule } from '../utils/createRule';\nimport {\n combineVisitors,\n createTemplateVisitor,\n type TemplateVisitor,\n} from '../utils/templates';\n\n/**\n * Elements whose text is markup, code or metadata rather than user-facing copy.\n * Extracting them would be wrong, so they are skipped by default.\n */\nconst DEFAULT_IGNORED_ELEMENTS = ['code', 'pre', 'script', 'style'];\n\nexport type NoRawTextOptions = {\n /**\n * JSX attributes whose string value is user-facing copy.\n * Defaults to the compiler's own list, so the linter flags exactly what\n * `intlayer extract` would rewrite.\n */\n attributes?: string[];\n /** JSX elements whose children are never translatable. */\n ignoreElements?: string[];\n /** Regular-expression sources for text that should never be reported. */\n ignorePatterns?: string[];\n /**\n * Also report string literals outside JSX (function arguments, object values,\n * …). Off by default: outside JSX the false-positive rate is much higher, and\n * the compiler's own extractor is the better first pass.\n */\n includeStringLiterals?: boolean;\n};\n\ntype MessageIds = 'rawText' | 'rawAttribute' | 'rawStringLiteral';\n\n/**\n * True for a content declaration file, where raw strings are the whole point.\n *\n * @param filename - Absolute path of the linted file.\n */\nconst isContentDeclarationFile = (filename: string): boolean =>\n FILE_EXTENSIONS.some((extension) => filename.endsWith(extension));\n\n/**\n * Walk up from a JSX child or attribute to the tag name that encloses it.\n *\n * @param node - The reported node.\n */\nconst getEnclosingElementName = (\n node: AstNodeWithParent | undefined\n): string | null => {\n let current = node;\n\n while (current) {\n if (current.type === 'JSXElement') {\n return getJsxElementName(current['openingElement'] as AstNode);\n }\n\n if (current.type === 'JSXOpeningElement') {\n return getJsxElementName(current);\n }\n\n current = current.parent;\n }\n\n return null;\n};\n\n/**\n * True when the string literal sits in a position that is structural rather\n * than user-facing: module specifiers, property keys, directives, and the\n * arguments of the Intlayer getters themselves.\n *\n * @param node - The Literal node.\n */\nconst isStructuralPosition = (node: AstNodeWithParent): boolean => {\n const parent = node.parent;\n\n if (!parent) return true;\n\n switch (parent.type) {\n case 'ImportDeclaration':\n case 'ImportExpression':\n case 'ExportNamedDeclaration':\n case 'ExportAllDeclaration':\n case 'JSXAttribute':\n case 'TSLiteralType':\n case 'TSEnumMember':\n case 'ExpressionStatement':\n return true;\n\n case 'Property':\n // Keys are structural; values are not.\n return parent['key'] === node && parent['computed'] !== true;\n\n case 'MemberExpression':\n return parent['property'] === node;\n\n default:\n return false;\n }\n};\n\nexport const noRawText = createRule<[NoRawTextOptions?], MessageIds>(\n 'no-raw-text',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report user-facing strings that are not declared in an Intlayer dictionary',\n },\n messages: {\n rawText:\n 'Untranslated text \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawAttribute:\n 'Untranslated `{{attribute}}` value \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n rawStringLiteral:\n 'Untranslated string \"{{text}}\". Move it to a dictionary — `npx intlayer extract` can do it for this file.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n attributes: { type: 'array', items: { type: 'string' } },\n ignoreElements: { type: 'array', items: { type: 'string' } },\n ignorePatterns: { type: 'array', items: { type: 'string' } },\n includeStringLiterals: { type: 'boolean' },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create: (context) => {\n const filename = context.filename;\n\n // Content declaration files hold the translations themselves.\n if (isContentDeclarationFile(filename)) return {};\n\n const [options = {}] = context.options;\n const attributes = new Set<string>(\n options.attributes ?? ATTRIBUTES_TO_EXTRACT\n );\n const ignoredElements = new Set<string>(\n options.ignoreElements ?? DEFAULT_IGNORED_ELEMENTS\n );\n const ignorePatterns = (options.ignorePatterns ?? []).map(\n (pattern) => new RegExp(pattern, 'u')\n );\n const includeStringLiterals = options.includeStringLiterals === true;\n\n /** True when the text is copy the extractor would pick up. */\n const isReportableText = (text: string): boolean =>\n shouldExtract(text) &&\n !ignorePatterns.some((pattern) => pattern.test(text.trim()));\n\n /** Collapse whitespace so multi-line JSX text reads well in the report. */\n const formatText = (text: string): string => {\n const normalized = text.replace(/\\s+/gu, ' ').trim();\n\n return normalized.length > 40\n ? `${normalized.slice(0, 40)}…`\n : normalized;\n };\n\n /**\n * Handlers for the markup languages that are not JSX — Vue, Svelte and\n * Angular templates. Reports through the same predicate and options as\n * the JSX path so a project gets identical results whatever it is written\n * in.\n */\n const templateVisitor = createTemplateVisitor(\n ({ node, text, attributeName, elementName }) => {\n if (elementName && ignoredElements.has(elementName)) return;\n\n if (attributeName !== null && !attributes.has(attributeName)) return;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: attributeName === null ? 'rawText' : 'rawAttribute',\n data: {\n text: formatText(text),\n ...(attributeName === null ? {} : { attribute: attributeName }),\n },\n });\n }\n );\n\n const scriptVisitor: TemplateVisitor = {\n JSXText: (node) => {\n const astNode = toAstNode(node as never);\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawText',\n data: { text: formatText(text) },\n });\n },\n\n JSXAttribute: (node) => {\n const astNode = toAstNode(node as never);\n const attributeName = getJsxAttributeName(astNode);\n\n if (!attributeName || !attributes.has(attributeName)) return;\n\n const valueNode = getJsxAttributeValueNode(astNode);\n const text = getStaticStringValue(valueNode);\n\n if (text === null || !isReportableText(text)) return;\n\n const elementName = getEnclosingElementName(astNode.parent);\n\n if (elementName && ignoredElements.has(elementName)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawAttribute',\n data: { attribute: attributeName, text: formatText(text) },\n });\n },\n\n Literal: (node) => {\n if (!includeStringLiterals) return;\n\n const astNode = toAstNode(node as never);\n\n if (typeof astNode['value'] !== 'string') return;\n\n if (isStructuralPosition(astNode)) return;\n\n const text = astNode['value'] as string;\n\n if (!isReportableText(text)) return;\n\n context.report({\n node: node as never,\n messageId: 'rawStringLiteral',\n data: { text: formatText(text) },\n });\n },\n };\n\n return combineVisitors(\n context.sourceCode.parserServices,\n scriptVisitor,\n templateVisitor\n );\n },\n }\n);\n"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_utils_ast = require('../utils/ast.cjs');
3
+ const require_utils_createRule = require('../utils/createRule.cjs');
4
+ const require_utils_callerMatcher = require('../utils/callerMatcher.cjs');
5
+
6
+ //#region src/rules/staticDictionaryKey.ts
7
+ /**
8
+ * True when the call may legitimately omit its namespace — next-intl's
9
+ * `useTranslations()` root scope derives the dictionary from each message id
10
+ * instead, so an absent argument is not a violation.
11
+ *
12
+ * @param descriptor - The matched registry entry.
13
+ */
14
+ const allowsMissingNamespace = (descriptor) => descriptor.allowRootScope === true;
15
+ /**
16
+ * Distinguish an interpolated template from any other runtime expression, so
17
+ * the report can name the actual problem.
18
+ *
19
+ * @param node - The namespace node.
20
+ */
21
+ const isInterpolatedTemplate = (node) => node.type === "TemplateLiteral" && (node["expressions"]?.length ?? 0) > 0;
22
+ const staticDictionaryKey = require_utils_createRule.createRule("static-dictionary-key", {
23
+ meta: {
24
+ type: "problem",
25
+ docs: { description: "Require the dictionary key of an Intlayer content read to be a static string literal" },
26
+ messages: {
27
+ dynamicKey: "The dictionary key passed to `{{caller}}` must be a string literal. A computed key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.",
28
+ dynamicTemplateKey: "The dictionary key passed to `{{caller}}` interpolates a value. Use a string literal — an interpolated key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.",
29
+ dynamicJsxKey: "The `{{attribute}}` of `<{{caller}}>` must be a static string. A computed value is invisible to the Intlayer compiler, which then cannot resolve the dictionary at build time."
30
+ }
31
+ },
32
+ create: (context) => {
33
+ let activeDescriptors = require_utils_callerMatcher.getActiveDescriptors(/* @__PURE__ */ new Set());
34
+ /** Report the namespace of a matched call when it is not static. */
35
+ const reportDynamicNamespace = (match) => {
36
+ const { descriptor, namespace } = match;
37
+ if (!namespace) return;
38
+ if (namespace.source.from === "fixed") return;
39
+ if (!namespace.node) return;
40
+ if (namespace.value !== null) return;
41
+ context.report({
42
+ node: namespace.node,
43
+ messageId: isInterpolatedTemplate(namespace.node) ? "dynamicTemplateKey" : "dynamicKey",
44
+ data: { caller: descriptor.callerName }
45
+ });
46
+ };
47
+ return {
48
+ Program: (node) => {
49
+ activeDescriptors = require_utils_callerMatcher.getActiveDescriptors(require_utils_callerMatcher.collectImportSources(require_utils_ast.toAstNode(node)));
50
+ },
51
+ CallExpression: (node) => {
52
+ const match = require_utils_callerMatcher.matchCaller(require_utils_ast.toAstNode(node), activeDescriptors);
53
+ if (!match) return;
54
+ if (!match.namespace && allowsMissingNamespace(match.descriptor)) return;
55
+ reportDynamicNamespace(match);
56
+ },
57
+ TaggedTemplateExpression: (node) => {
58
+ const match = require_utils_callerMatcher.matchCaller(require_utils_ast.toAstNode(node), activeDescriptors);
59
+ if (match) reportDynamicNamespace(match);
60
+ },
61
+ JSXOpeningElement: (node) => {
62
+ const astNode = require_utils_ast.toAstNode(node);
63
+ const descriptor = require_utils_callerMatcher.matchJsxCaller(require_utils_ast.getJsxElementName(astNode), activeDescriptors);
64
+ if (!descriptor) return;
65
+ const attributes = astNode["attributes"] ?? [];
66
+ const targetAttributes = [descriptor.jsxIdAttribute, descriptor.jsxNamespaceAttribute].filter((name) => typeof name === "string");
67
+ for (const attribute of attributes) {
68
+ const attributeName = require_utils_ast.getJsxAttributeName(attribute);
69
+ if (!attributeName || !targetAttributes.includes(attributeName)) continue;
70
+ const valueNode = require_utils_ast.getJsxAttributeValueNode(attribute);
71
+ if (!valueNode) continue;
72
+ if (require_utils_ast.getStaticStringValue(valueNode) !== null) continue;
73
+ context.report({
74
+ node: attribute,
75
+ messageId: "dynamicJsxKey",
76
+ data: {
77
+ attribute: attributeName,
78
+ caller: descriptor.callerName
79
+ }
80
+ });
81
+ }
82
+ }
83
+ };
84
+ }
85
+ });
86
+
87
+ //#endregion
88
+ exports.staticDictionaryKey = staticDictionaryKey;
89
+ //# sourceMappingURL=staticDictionaryKey.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staticDictionaryKey.cjs","names":["createRule","getActiveDescriptors","collectImportSources","toAstNode","matchCaller","matchJsxCaller","getJsxElementName","getJsxAttributeName","getJsxAttributeValueNode","getStaticStringValue"],"sources":["../../../src/rules/staticDictionaryKey.ts"],"sourcesContent":["import type { CallerDescriptor } from '@intlayer/config/callers';\nimport {\n type AstNode,\n getJsxAttributeName,\n getJsxAttributeValueNode,\n getJsxElementName,\n getStaticStringValue,\n toAstNode,\n} from '../utils/ast';\nimport {\n type CallerMatch,\n collectImportSources,\n getActiveDescriptors,\n matchCaller,\n matchJsxCaller,\n} from '../utils/callerMatcher';\nimport { createRule } from '../utils/createRule';\n\ntype MessageIds = 'dynamicKey' | 'dynamicTemplateKey' | 'dynamicJsxKey';\n\n/**\n * True when the call may legitimately omit its namespace — next-intl's\n * `useTranslations()` root scope derives the dictionary from each message id\n * instead, so an absent argument is not a violation.\n *\n * @param descriptor - The matched registry entry.\n */\nconst allowsMissingNamespace = (descriptor: CallerDescriptor): boolean =>\n descriptor.allowRootScope === true;\n\n/**\n * Distinguish an interpolated template from any other runtime expression, so\n * the report can name the actual problem.\n *\n * @param node - The namespace node.\n */\nconst isInterpolatedTemplate = (node: AstNode): boolean =>\n node.type === 'TemplateLiteral' &&\n ((node['expressions'] as unknown[] | undefined)?.length ?? 0) > 0;\n\nexport const staticDictionaryKey = createRule<[], MessageIds>(\n 'static-dictionary-key',\n {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require the dictionary key of an Intlayer content read to be a static string literal',\n },\n messages: {\n dynamicKey:\n 'The dictionary key passed to `{{caller}}` must be a string literal. A computed key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.',\n dynamicTemplateKey:\n 'The dictionary key passed to `{{caller}}` interpolates a value. Use a string literal — an interpolated key is invisible to the Intlayer compiler, which then skips the optimize pass and bundles every dictionary.',\n dynamicJsxKey:\n 'The `{{attribute}}` of `<{{caller}}>` must be a static string. A computed value is invisible to the Intlayer compiler, which then cannot resolve the dictionary at build time.',\n },\n },\n\n create: (context) => {\n let activeDescriptors = getActiveDescriptors(new Set());\n\n /** Report the namespace of a matched call when it is not static. */\n const reportDynamicNamespace = (match: CallerMatch): void => {\n const { descriptor, namespace } = match;\n\n if (!namespace) return;\n\n // A `fixed` namespace is a compile-time constant by construction.\n if (namespace.source.from === 'fixed') return;\n\n if (!namespace.node) {\n // Nothing was passed at all — only a problem for callers that require it.\n return;\n }\n\n if (namespace.value !== null) return;\n\n context.report({\n node: namespace.node as never,\n messageId: isInterpolatedTemplate(namespace.node)\n ? 'dynamicTemplateKey'\n : 'dynamicKey',\n data: { caller: descriptor.callerName },\n });\n };\n\n return {\n Program: (node) => {\n activeDescriptors = getActiveDescriptors(\n collectImportSources(toAstNode(node))\n );\n },\n\n CallExpression: (node) => {\n const match = matchCaller(toAstNode(node), activeDescriptors);\n\n if (!match) return;\n\n if (!match.namespace && allowsMissingNamespace(match.descriptor)) {\n return;\n }\n\n reportDynamicNamespace(match);\n },\n\n TaggedTemplateExpression: (node) => {\n const match = matchCaller(toAstNode(node), activeDescriptors);\n\n if (match) reportDynamicNamespace(match);\n },\n\n JSXOpeningElement: (node) => {\n const astNode = toAstNode(node);\n const descriptor = matchJsxCaller(\n getJsxElementName(astNode),\n activeDescriptors\n );\n\n if (!descriptor) return;\n\n const attributes =\n (astNode['attributes'] as AstNode[] | undefined) ?? [];\n\n const targetAttributes = [\n descriptor.jsxIdAttribute,\n descriptor.jsxNamespaceAttribute,\n ].filter((name): name is string => typeof name === 'string');\n\n for (const attribute of attributes) {\n const attributeName = getJsxAttributeName(attribute);\n\n if (!attributeName || !targetAttributes.includes(attributeName)) {\n continue;\n }\n\n const valueNode = getJsxAttributeValueNode(attribute);\n\n if (!valueNode) continue;\n\n if (getStaticStringValue(valueNode) !== null) continue;\n\n context.report({\n node: attribute as never,\n messageId: 'dynamicJsxKey',\n data: {\n attribute: attributeName,\n caller: descriptor.callerName,\n },\n });\n }\n },\n };\n },\n }\n);\n"],"mappings":";;;;;;;;;;;;;AA2BA,MAAM,0BAA0B,eAC9B,WAAW,mBAAmB;;;;;;;AAQhC,MAAM,0BAA0B,SAC9B,KAAK,SAAS,sBACZ,KAAK,cAAc,EAA4B,UAAU,KAAK;AAElE,MAAa,sBAAsBA,oCACjC,yBACA;CACE,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACJ;EACA,UAAU;GACR,YACE;GACF,oBACE;GACF,eACE;EACJ;CACF;CAEA,SAAS,YAAY;EACnB,IAAI,oBAAoBC,iEAAqB,IAAI,IAAI,CAAC;;EAGtD,MAAM,0BAA0B,UAA6B;GAC3D,MAAM,EAAE,YAAY,cAAc;GAElC,IAAI,CAAC,WAAW;GAGhB,IAAI,UAAU,OAAO,SAAS,SAAS;GAEvC,IAAI,CAAC,UAAU,MAEb;GAGF,IAAI,UAAU,UAAU,MAAM;GAE9B,QAAQ,OAAO;IACb,MAAM,UAAU;IAChB,WAAW,uBAAuB,UAAU,IAAI,IAC5C,uBACA;IACJ,MAAM,EAAE,QAAQ,WAAW,WAAW;GACxC,CAAC;EACH;EAEA,OAAO;GACL,UAAU,SAAS;IACjB,oBAAoBA,iDAClBC,iDAAqBC,4BAAU,IAAI,CAAC,CACtC;GACF;GAEA,iBAAiB,SAAS;IACxB,MAAM,QAAQC,wCAAYD,4BAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,CAAC,OAAO;IAEZ,IAAI,CAAC,MAAM,aAAa,uBAAuB,MAAM,UAAU,GAC7D;IAGF,uBAAuB,KAAK;GAC9B;GAEA,2BAA2B,SAAS;IAClC,MAAM,QAAQC,wCAAYD,4BAAU,IAAI,GAAG,iBAAiB;IAE5D,IAAI,OAAO,uBAAuB,KAAK;GACzC;GAEA,oBAAoB,SAAS;IAC3B,MAAM,UAAUA,4BAAU,IAAI;IAC9B,MAAM,aAAaE,2CACjBC,oCAAkB,OAAO,GACzB,iBACF;IAEA,IAAI,CAAC,YAAY;IAEjB,MAAM,aACH,QAAQ,iBAA2C,CAAC;IAEvD,MAAM,mBAAmB,CACvB,WAAW,gBACX,WAAW,qBACb,CAAC,CAAC,QAAQ,SAAyB,OAAO,SAAS,QAAQ;IAE3D,KAAK,MAAM,aAAa,YAAY;KAClC,MAAM,gBAAgBC,sCAAoB,SAAS;KAEnD,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,SAAS,aAAa,GAC5D;KAGF,MAAM,YAAYC,2CAAyB,SAAS;KAEpD,IAAI,CAAC,WAAW;KAEhB,IAAIC,uCAAqB,SAAS,MAAM,MAAM;KAE9C,QAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM;OACJ,WAAW;OACX,QAAQ,WAAW;MACrB;KACF,CAAC;IACH;GACF;EACF;CACF;AACF,CACF"}
@@ -0,0 +1,151 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region src/utils/ast.ts
3
+ /**
4
+ * TypeScript wrapper expressions that carry no runtime meaning. The Intlayer
5
+ * compiler unwraps them before reading a value, so the rules must too —
6
+ * `useIntlayer('home' as const)` is every bit as static as `useIntlayer('home')`.
7
+ */
8
+ const TRANSPARENT_EXPRESSION_TYPES = /* @__PURE__ */ new Set([
9
+ "TSAsExpression",
10
+ "TSSatisfiesExpression",
11
+ "TSNonNullExpression",
12
+ "TSTypeAssertion",
13
+ "TSInstantiationExpression"
14
+ ]);
15
+ /**
16
+ * Strip TypeScript-only and parenthesised wrappers to reach the expression that
17
+ * actually produces the value.
18
+ *
19
+ * @param node - Expression to unwrap; `null`/`undefined` passes straight through.
20
+ */
21
+ const unwrapExpression = (node) => {
22
+ let current = node ?? null;
23
+ while (current && (TRANSPARENT_EXPRESSION_TYPES.has(current.type) || current.type === "ChainExpression")) current = current["expression"] ?? null;
24
+ return current ?? null;
25
+ };
26
+ /**
27
+ * Read the compile-time string value of an expression, or `null` when the value
28
+ * can only be known at runtime.
29
+ *
30
+ * Deliberately does **not** resolve identifiers, even ones bound to a string
31
+ * constant: the Babel/SWC passes read the argument node itself, so a value that
32
+ * needs constant folding is a value the compiler will not see.
33
+ *
34
+ * @param node - Expression to evaluate.
35
+ */
36
+ const getStaticStringValue = (node) => {
37
+ const expression = unwrapExpression(node);
38
+ if (!expression) return null;
39
+ if (expression.type === "Literal") {
40
+ const value = expression["value"];
41
+ return typeof value === "string" ? value : null;
42
+ }
43
+ if (expression.type === "TemplateLiteral") {
44
+ const expressions = expression["expressions"];
45
+ const quasis = expression["quasis"];
46
+ if (expressions?.length !== 0 || quasis?.length !== 1) return null;
47
+ const cooked = (quasis[0]?.["value"])?.cooked;
48
+ return typeof cooked === "string" ? cooked : null;
49
+ }
50
+ return null;
51
+ };
52
+ /**
53
+ * Name of the function being called, plus whether it was reached as a method.
54
+ *
55
+ * `matchAsMethod` descriptors (`i18n.getFixedT(…)`, `intl.formatMessage(…)`)
56
+ * only match the member form, so the two cases must stay distinguishable.
57
+ *
58
+ * @param callee - The `callee` of a CallExpression.
59
+ */
60
+ const getCalleeName = (callee) => {
61
+ const expression = unwrapExpression(callee);
62
+ if (!expression) return null;
63
+ if (expression.type === "Identifier") return {
64
+ name: expression["name"],
65
+ isMethod: false
66
+ };
67
+ if (expression.type === "MemberExpression" && !expression["computed"]) {
68
+ const property = expression["property"];
69
+ if (property?.type === "Identifier") return {
70
+ name: property["name"],
71
+ isMethod: true
72
+ };
73
+ }
74
+ return null;
75
+ };
76
+ /**
77
+ * Read a string-valued property from an object-literal argument, e.g. the
78
+ * `namespace` of `getTranslations({ namespace: 'home' })`.
79
+ *
80
+ * Returns the property's value node even when it is not static, so callers can
81
+ * tell "absent" (`null`) from "present but dynamic" (a node, no string).
82
+ *
83
+ * @param node - The argument expected to be an object expression.
84
+ * @param propertyName - Property to look up.
85
+ */
86
+ const getObjectPropertyValueNode = (node, propertyName) => {
87
+ const expression = unwrapExpression(node);
88
+ if (expression?.type !== "ObjectExpression") return null;
89
+ const properties = expression["properties"] ?? [];
90
+ for (const property of properties) {
91
+ if (property.type !== "Property") continue;
92
+ const key = property["key"];
93
+ const computed = property["computed"] === true;
94
+ if ((key?.type === "Identifier" && !computed ? key["name"] : key?.type === "Literal" ? String(key["value"]) : null) === propertyName) return property["value"] ?? null;
95
+ }
96
+ return null;
97
+ };
98
+ /**
99
+ * Name of a JSX attribute, or `null` for spread attributes and namespaced
100
+ * names the plugin does not target.
101
+ *
102
+ * @param attribute - A member of a JSX opening element's `attributes`.
103
+ */
104
+ const getJsxAttributeName = (attribute) => {
105
+ if (attribute?.type !== "JSXAttribute") return null;
106
+ const name = attribute["name"];
107
+ return name?.type === "JSXIdentifier" ? name["name"] : null;
108
+ };
109
+ /**
110
+ * Value node of a JSX attribute, unwrapping `{'…'}` expression containers so
111
+ * `title="Text"` and `title={'Text'}` are treated alike.
112
+ *
113
+ * @param attribute - A JSX attribute node.
114
+ */
115
+ const getJsxAttributeValueNode = (attribute) => {
116
+ const value = attribute?.["value"] ?? null;
117
+ if (value?.type === "JSXExpressionContainer") return unwrapExpression(value["expression"] ?? null);
118
+ return value;
119
+ };
120
+ /**
121
+ * Tag name of the JSX element an attribute or child belongs to, e.g. `'code'`
122
+ * for `<code>`. Member and namespaced element names are joined with `.`.
123
+ *
124
+ * @param openingElement - A JSXOpeningElement node.
125
+ */
126
+ const getJsxElementName = (openingElement) => {
127
+ const readName = (node) => {
128
+ if (!node) return null;
129
+ if (node.type === "JSXIdentifier") return node["name"];
130
+ if (node.type === "JSXMemberExpression") {
131
+ const object = readName(node["object"]);
132
+ const property = readName(node["property"]);
133
+ return object && property ? `${object}.${property}` : null;
134
+ }
135
+ return null;
136
+ };
137
+ return readName(openingElement?.["name"]);
138
+ };
139
+ /** Narrow an ESLint visitor node to the loose {@link AstNode} shape. */
140
+ const toAstNode = (node) => node;
141
+
142
+ //#endregion
143
+ exports.getCalleeName = getCalleeName;
144
+ exports.getJsxAttributeName = getJsxAttributeName;
145
+ exports.getJsxAttributeValueNode = getJsxAttributeValueNode;
146
+ exports.getJsxElementName = getJsxElementName;
147
+ exports.getObjectPropertyValueNode = getObjectPropertyValueNode;
148
+ exports.getStaticStringValue = getStaticStringValue;
149
+ exports.toAstNode = toAstNode;
150
+ exports.unwrapExpression = unwrapExpression;
151
+ //# sourceMappingURL=ast.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.cjs","names":[],"sources":["../../../src/utils/ast.ts"],"sourcesContent":["import type { Node as EsTreeNode } from 'estree';\n\n/**\n * Minimal structural models for the nodes this plugin inspects that are not\n * part of the ESTree spec — JSX (all parsers) and the TypeScript-only wrapper\n * expressions emitted by `@typescript-eslint/parser`.\n *\n * They are declared here rather than imported from `@typescript-eslint/types`\n * so the plugin stays parser-agnostic: the same rules run under `espree`,\n * `@typescript-eslint/parser`, `vue-eslint-parser` and oxlint's JS plugin\n * bridge, none of which agree on a type package.\n */\n\n/** Any AST node, whatever the parser. Narrowed through the guards below. */\nexport type AstNode = {\n type: string;\n [key: string]: unknown;\n};\n\n/** An AST node as ESLint hands it to a visitor — always linked to its parent. */\nexport type AstNodeWithParent = AstNode & {\n parent?: AstNodeWithParent;\n};\n\n/** `<div>Some text</div>` — the raw text between two JSX tags. */\nexport type JsxTextNode = AstNode & {\n type: 'JSXText';\n value: string;\n};\n\n/** `title=\"Some text\"` on a JSX element. */\nexport type JsxAttributeNode = AstNode & {\n type: 'JSXAttribute';\n name: AstNode;\n value: AstNode | null;\n};\n\n/** The opening tag of a JSX element, holding its name and attributes. */\nexport type JsxOpeningElementNode = AstNode & {\n type: 'JSXOpeningElement';\n name: AstNode;\n attributes: AstNode[];\n};\n\n/**\n * TypeScript wrapper expressions that carry no runtime meaning. The Intlayer\n * compiler unwraps them before reading a value, so the rules must too —\n * `useIntlayer('home' as const)` is every bit as static as `useIntlayer('home')`.\n */\nconst TRANSPARENT_EXPRESSION_TYPES = new Set([\n 'TSAsExpression',\n 'TSSatisfiesExpression',\n 'TSNonNullExpression',\n 'TSTypeAssertion',\n 'TSInstantiationExpression',\n]);\n\n/**\n * Strip TypeScript-only and parenthesised wrappers to reach the expression that\n * actually produces the value.\n *\n * @param node - Expression to unwrap; `null`/`undefined` passes straight through.\n */\nexport const unwrapExpression = <T extends AstNode>(\n node: T | null | undefined\n): T | null => {\n let current: AstNode | null = node ?? null;\n\n while (\n current &&\n (TRANSPARENT_EXPRESSION_TYPES.has(current.type) ||\n current.type === 'ChainExpression')\n ) {\n current = (current['expression'] as AstNode | undefined) ?? null;\n }\n\n return (current as T | null) ?? null;\n};\n\n/**\n * Read the compile-time string value of an expression, or `null` when the value\n * can only be known at runtime.\n *\n * Deliberately does **not** resolve identifiers, even ones bound to a string\n * constant: the Babel/SWC passes read the argument node itself, so a value that\n * needs constant folding is a value the compiler will not see.\n *\n * @param node - Expression to evaluate.\n */\nexport const getStaticStringValue = (\n node: AstNode | null | undefined\n): string | null => {\n const expression = unwrapExpression(node);\n\n if (!expression) return null;\n\n if (expression.type === 'Literal') {\n const value = expression['value'];\n\n return typeof value === 'string' ? value : null;\n }\n\n // A template literal is static only when it interpolates nothing.\n if (expression.type === 'TemplateLiteral') {\n const expressions = expression['expressions'] as unknown[] | undefined;\n const quasis = expression['quasis'] as AstNode[] | undefined;\n\n if (expressions?.length !== 0 || quasis?.length !== 1) return null;\n\n const cooked = (quasis[0]?.['value'] as { cooked?: string } | undefined)\n ?.cooked;\n\n return typeof cooked === 'string' ? cooked : null;\n }\n\n return null;\n};\n\n/**\n * Name of the function being called, plus whether it was reached as a method.\n *\n * `matchAsMethod` descriptors (`i18n.getFixedT(…)`, `intl.formatMessage(…)`)\n * only match the member form, so the two cases must stay distinguishable.\n *\n * @param callee - The `callee` of a CallExpression.\n */\nexport const getCalleeName = (\n callee: AstNode | null | undefined\n): { name: string; isMethod: boolean } | null => {\n const expression = unwrapExpression(callee);\n\n if (!expression) return null;\n\n if (expression.type === 'Identifier') {\n return { name: expression['name'] as string, isMethod: false };\n }\n\n if (expression.type === 'MemberExpression' && !expression['computed']) {\n const property = expression['property'] as AstNode | undefined;\n\n if (property?.type === 'Identifier') {\n return { name: property['name'] as string, isMethod: true };\n }\n }\n\n return null;\n};\n\n/**\n * Read a string-valued property from an object-literal argument, e.g. the\n * `namespace` of `getTranslations({ namespace: 'home' })`.\n *\n * Returns the property's value node even when it is not static, so callers can\n * tell \"absent\" (`null`) from \"present but dynamic\" (a node, no string).\n *\n * @param node - The argument expected to be an object expression.\n * @param propertyName - Property to look up.\n */\nexport const getObjectPropertyValueNode = (\n node: AstNode | null | undefined,\n propertyName: string\n): AstNode | null => {\n const expression = unwrapExpression(node);\n\n if (expression?.type !== 'ObjectExpression') return null;\n\n const properties = (expression['properties'] as AstNode[] | undefined) ?? [];\n\n for (const property of properties) {\n if (property.type !== 'Property') continue;\n\n const key = property['key'] as AstNode | undefined;\n const computed = property['computed'] === true;\n\n const keyName =\n key?.type === 'Identifier' && !computed\n ? (key['name'] as string)\n : key?.type === 'Literal'\n ? String(key['value'])\n : null;\n\n if (keyName === propertyName) {\n return (property['value'] as AstNode | undefined) ?? null;\n }\n }\n\n return null;\n};\n\n/**\n * Name of a JSX attribute, or `null` for spread attributes and namespaced\n * names the plugin does not target.\n *\n * @param attribute - A member of a JSX opening element's `attributes`.\n */\nexport const getJsxAttributeName = (\n attribute: AstNode | null | undefined\n): string | null => {\n if (attribute?.type !== 'JSXAttribute') return null;\n\n const name = attribute['name'] as AstNode | undefined;\n\n return name?.type === 'JSXIdentifier' ? (name['name'] as string) : null;\n};\n\n/**\n * Value node of a JSX attribute, unwrapping `{'…'}` expression containers so\n * `title=\"Text\"` and `title={'Text'}` are treated alike.\n *\n * @param attribute - A JSX attribute node.\n */\nexport const getJsxAttributeValueNode = (\n attribute: AstNode | null | undefined\n): AstNode | null => {\n const value = (attribute?.['value'] as AstNode | undefined) ?? null;\n\n if (value?.type === 'JSXExpressionContainer') {\n return unwrapExpression((value['expression'] as AstNode) ?? null);\n }\n\n return value;\n};\n\n/**\n * Tag name of the JSX element an attribute or child belongs to, e.g. `'code'`\n * for `<code>`. Member and namespaced element names are joined with `.`.\n *\n * @param openingElement - A JSXOpeningElement node.\n */\nexport const getJsxElementName = (\n openingElement: AstNode | null | undefined\n): string | null => {\n const readName = (node: AstNode | null | undefined): string | null => {\n if (!node) return null;\n\n if (node.type === 'JSXIdentifier') return node['name'] as string;\n\n if (node.type === 'JSXMemberExpression') {\n const object = readName(node['object'] as AstNode);\n const property = readName(node['property'] as AstNode);\n\n return object && property ? `${object}.${property}` : null;\n }\n\n return null;\n };\n\n return readName(openingElement?.['name'] as AstNode | undefined);\n};\n\n/** Narrow an ESLint visitor node to the loose {@link AstNode} shape. */\nexport const toAstNode = (node: EsTreeNode | AstNode): AstNodeWithParent =>\n node as unknown as AstNodeWithParent;\n"],"mappings":";;;;;;;AAiDA,MAAM,+CAA+B,IAAI,IAAI;CAC3C;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;AAQD,MAAa,oBACX,SACa;CACb,IAAI,UAA0B,QAAQ;CAEtC,OACE,YACC,6BAA6B,IAAI,QAAQ,IAAI,KAC5C,QAAQ,SAAS,oBAEnB,UAAW,QAAQ,iBAAyC;CAG9D,OAAQ,WAAwB;AAClC;;;;;;;;;;;AAYA,MAAa,wBACX,SACkB;CAClB,MAAM,aAAa,iBAAiB,IAAI;CAExC,IAAI,CAAC,YAAY,OAAO;CAExB,IAAI,WAAW,SAAS,WAAW;EACjC,MAAM,QAAQ,WAAW;EAEzB,OAAO,OAAO,UAAU,WAAW,QAAQ;CAC7C;CAGA,IAAI,WAAW,SAAS,mBAAmB;EACzC,MAAM,cAAc,WAAW;EAC/B,MAAM,SAAS,WAAW;EAE1B,IAAI,aAAa,WAAW,KAAK,QAAQ,WAAW,GAAG,OAAO;EAE9D,MAAM,UAAU,OAAO,EAAE,GAAG,SAAQ,EAChC;EAEJ,OAAO,OAAO,WAAW,WAAW,SAAS;CAC/C;CAEA,OAAO;AACT;;;;;;;;;AAUA,MAAa,iBACX,WAC+C;CAC/C,MAAM,aAAa,iBAAiB,MAAM;CAE1C,IAAI,CAAC,YAAY,OAAO;CAExB,IAAI,WAAW,SAAS,cACtB,OAAO;EAAE,MAAM,WAAW;EAAmB,UAAU;CAAM;CAG/D,IAAI,WAAW,SAAS,sBAAsB,CAAC,WAAW,aAAa;EACrE,MAAM,WAAW,WAAW;EAE5B,IAAI,UAAU,SAAS,cACrB,OAAO;GAAE,MAAM,SAAS;GAAmB,UAAU;EAAK;CAE9D;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,MAAa,8BACX,MACA,iBACmB;CACnB,MAAM,aAAa,iBAAiB,IAAI;CAExC,IAAI,YAAY,SAAS,oBAAoB,OAAO;CAEpD,MAAM,aAAc,WAAW,iBAA2C,CAAC;CAE3E,KAAK,MAAM,YAAY,YAAY;EACjC,IAAI,SAAS,SAAS,YAAY;EAElC,MAAM,MAAM,SAAS;EACrB,MAAM,WAAW,SAAS,gBAAgB;EAS1C,KANE,KAAK,SAAS,gBAAgB,CAAC,WAC1B,IAAI,UACL,KAAK,SAAS,YACZ,OAAO,IAAI,QAAQ,IACnB,UAEQ,cACd,OAAQ,SAAS,YAAoC;CAEzD;CAEA,OAAO;AACT;;;;;;;AAQA,MAAa,uBACX,cACkB;CAClB,IAAI,WAAW,SAAS,gBAAgB,OAAO;CAE/C,MAAM,OAAO,UAAU;CAEvB,OAAO,MAAM,SAAS,kBAAmB,KAAK,UAAqB;AACrE;;;;;;;AAQA,MAAa,4BACX,cACmB;CACnB,MAAM,QAAS,YAAY,YAAoC;CAE/D,IAAI,OAAO,SAAS,0BAClB,OAAO,iBAAkB,MAAM,iBAA6B,IAAI;CAGlE,OAAO;AACT;;;;;;;AAQA,MAAa,qBACX,mBACkB;CAClB,MAAM,YAAY,SAAoD;EACpE,IAAI,CAAC,MAAM,OAAO;EAElB,IAAI,KAAK,SAAS,iBAAiB,OAAO,KAAK;EAE/C,IAAI,KAAK,SAAS,uBAAuB;GACvC,MAAM,SAAS,SAAS,KAAK,SAAoB;GACjD,MAAM,WAAW,SAAS,KAAK,WAAsB;GAErD,OAAO,UAAU,WAAW,GAAG,OAAO,GAAG,aAAa;EACxD;EAEA,OAAO;CACT;CAEA,OAAO,SAAS,iBAAiB,OAA8B;AACjE;;AAGA,MAAa,aAAa,SACxB"}
@@ -0,0 +1,130 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_utils_ast = require('./ast.cjs');
3
+ let _intlayer_config_callers = require("@intlayer/config/callers");
4
+
5
+ //#region src/utils/callerMatcher.ts
6
+ /**
7
+ * Module specifiers imported by a file, as they are written in source.
8
+ *
9
+ * `isCallerActive` gates generic caller names (lingui's `t` / `_`) on the file
10
+ * actually importing the library, so this must be collected before matching.
11
+ *
12
+ * @param program - The Program node of the analysed file.
13
+ */
14
+ const collectImportSources = (program) => {
15
+ const sources = /* @__PURE__ */ new Set();
16
+ const body = program["body"] ?? [];
17
+ for (const statement of body) {
18
+ if (statement.type !== "ImportDeclaration" && statement.type !== "ExportNamedDeclaration" && statement.type !== "ExportAllDeclaration") continue;
19
+ const value = statement["source"]?.["value"];
20
+ if (typeof value === "string") sources.add(value);
21
+ }
22
+ return sources;
23
+ };
24
+ /**
25
+ * The registry entries that may match inside a file, given what it imports.
26
+ *
27
+ * @param importSources - Specifiers collected by {@link collectImportSources}.
28
+ */
29
+ const getActiveDescriptors = (importSources) => _intlayer_config_callers.ALL_CALLERS.filter((descriptor) => (0, _intlayer_config_callers.isCallerActive)(descriptor, importSources));
30
+ /**
31
+ * Resolve one {@link CallerValueSource} against a call's arguments.
32
+ *
33
+ * @param source - The registry rule describing where to look.
34
+ * @param callArguments - Arguments of the call expression.
35
+ * @param descriptor - The caller being matched, needed to disambiguate overloads.
36
+ */
37
+ const resolveNamespaceSource = (source, callArguments, descriptor) => {
38
+ if (source.from === "fixed") return {
39
+ node: null,
40
+ value: source.value,
41
+ source
42
+ };
43
+ if (source.from === "argument") {
44
+ const argument = callArguments[source.index] ?? null;
45
+ if (!argument) return null;
46
+ if (argument.type === "SpreadElement") return {
47
+ node: argument,
48
+ value: null,
49
+ source
50
+ };
51
+ if (argument.type === "ObjectExpression" && descriptor.namespaceSources.some((candidate) => candidate.from === "option" && candidate.argumentIndex === source.index)) return null;
52
+ return {
53
+ node: argument,
54
+ value: require_utils_ast.getStaticStringValue(argument),
55
+ source
56
+ };
57
+ }
58
+ if (source.from === "option") {
59
+ const optionsArgument = callArguments[source.argumentIndex] ?? null;
60
+ const valueNode = require_utils_ast.getObjectPropertyValueNode(optionsArgument, source.property);
61
+ if (!valueNode) return null;
62
+ return {
63
+ node: valueNode,
64
+ value: require_utils_ast.getStaticStringValue(valueNode),
65
+ source
66
+ };
67
+ }
68
+ const [firstArgument] = callArguments;
69
+ const messageId = require_utils_ast.getStaticStringValue(firstArgument);
70
+ return {
71
+ node: firstArgument ?? null,
72
+ value: messageId ? messageId.split(".")[0] ?? null : null,
73
+ source
74
+ };
75
+ };
76
+ /**
77
+ * Match a call expression against the shared caller registry.
78
+ *
79
+ * Tries every `namespaceSources` entry in declaration order and keeps the first
80
+ * that yields a node or a value — mirroring how `@intlayer/babel` picks an
81
+ * overload, so the linter and the compiler agree on what a call site means.
82
+ *
83
+ * @param callNode - A CallExpression (or TaggedTemplateExpression) node.
84
+ * @param activeDescriptors - Result of {@link getActiveDescriptors}.
85
+ */
86
+ const matchCaller = (callNode, activeDescriptors) => {
87
+ const isTaggedTemplate = callNode.type === "TaggedTemplateExpression";
88
+ if (callNode.type !== "CallExpression" && !isTaggedTemplate) return null;
89
+ const callee = require_utils_ast.getCalleeName(callNode[isTaggedTemplate ? "tag" : "callee"] ?? null);
90
+ if (!callee) return null;
91
+ const callArguments = (callNode["arguments"] ?? []).map((argument) => argument.type === "SpreadElement" ? argument : require_utils_ast.unwrapExpression(argument)).filter((argument) => argument !== null);
92
+ for (const descriptor of activeDescriptors) {
93
+ if (descriptor.callerName !== callee.name) continue;
94
+ if (callee.isMethod && descriptor.matchAsMethod !== true) continue;
95
+ if (isTaggedTemplate && descriptor.matchAsTaggedTemplate !== true) continue;
96
+ let fallback = null;
97
+ for (const source of descriptor.namespaceSources) {
98
+ const namespace = resolveNamespaceSource(source, callArguments, descriptor);
99
+ if (!namespace) continue;
100
+ if (namespace.value !== null) return {
101
+ descriptor,
102
+ namespace
103
+ };
104
+ fallback ??= namespace;
105
+ }
106
+ return {
107
+ descriptor,
108
+ namespace: fallback
109
+ };
110
+ }
111
+ return null;
112
+ };
113
+ /**
114
+ * Match a JSX element against the registry's JSX-shaped callers,
115
+ * e.g. `<FormattedMessage id="home.title" />` or `<Trans ns="home" …/>`.
116
+ *
117
+ * @param elementName - Tag name of the element.
118
+ * @param activeDescriptors - Result of {@link getActiveDescriptors}.
119
+ */
120
+ const matchJsxCaller = (elementName, activeDescriptors) => {
121
+ if (!elementName) return null;
122
+ return activeDescriptors.find((descriptor) => descriptor.callerName === elementName && descriptor.jsxIdAttribute !== void 0) ?? null;
123
+ };
124
+
125
+ //#endregion
126
+ exports.collectImportSources = collectImportSources;
127
+ exports.getActiveDescriptors = getActiveDescriptors;
128
+ exports.matchCaller = matchCaller;
129
+ exports.matchJsxCaller = matchJsxCaller;
130
+ //# sourceMappingURL=callerMatcher.cjs.map