intor-cli 0.0.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 (92) hide show
  1. package/README.md +46 -0
  2. package/package.json +73 -0
  3. package/src/build/build-schemas/build-schemas.ts +13 -0
  4. package/src/build/build-schemas/index.ts +1 -0
  5. package/src/build/build-types/build-types.ts +46 -0
  6. package/src/build/build-types/index.ts +1 -0
  7. package/src/build/build-types/output/append-config-block.ts +34 -0
  8. package/src/build/build-types/output/append-footer.ts +5 -0
  9. package/src/build/build-types/output/append-header.ts +11 -0
  10. package/src/build/build-types/output/index.ts +3 -0
  11. package/src/build/build-types/utils/indent.ts +3 -0
  12. package/src/build/build-types/utils/render-infer-node.ts +29 -0
  13. package/src/build/index.ts +3 -0
  14. package/src/build/types.ts +19 -0
  15. package/src/cli/commands/check.ts +50 -0
  16. package/src/cli/commands/generate.ts +61 -0
  17. package/src/cli/index.ts +24 -0
  18. package/src/core/collect-messages/collect-runtime-messages.ts +76 -0
  19. package/src/core/collect-messages/index.ts +1 -0
  20. package/src/core/collect-messages/readers.ts +23 -0
  21. package/src/core/collect-messages/resolve-messages-reader.ts +57 -0
  22. package/src/core/constants/extra-exts.ts +2 -0
  23. package/src/core/constants/generated-files.ts +3 -0
  24. package/src/core/constants/index.ts +7 -0
  25. package/src/core/diagnostics/collect.ts +59 -0
  26. package/src/core/diagnostics/group.ts +41 -0
  27. package/src/core/diagnostics/index.ts +2 -0
  28. package/src/core/diagnostics/messages.ts +51 -0
  29. package/src/core/diagnostics/rules/enforce-missing-replacements.ts +55 -0
  30. package/src/core/diagnostics/rules/enforce-missing-rich.ts +55 -0
  31. package/src/core/diagnostics/rules/key/empty.ts +34 -0
  32. package/src/core/diagnostics/rules/key/index.ts +2 -0
  33. package/src/core/diagnostics/rules/key/not-found.ts +43 -0
  34. package/src/core/diagnostics/rules/replacement/index.ts +3 -0
  35. package/src/core/diagnostics/rules/replacement/missing.ts +48 -0
  36. package/src/core/diagnostics/rules/replacement/not-allowed.ts +43 -0
  37. package/src/core/diagnostics/rules/replacement/unused.ts +48 -0
  38. package/src/core/diagnostics/rules/rich/index.ts +3 -0
  39. package/src/core/diagnostics/rules/rich/missing.ts +48 -0
  40. package/src/core/diagnostics/rules/rich/not-allowed.ts +43 -0
  41. package/src/core/diagnostics/rules/rich/unused.ts +48 -0
  42. package/src/core/diagnostics/types.ts +21 -0
  43. package/src/core/diagnostics/utils/get-schema-node-at-path.ts +29 -0
  44. package/src/core/diagnostics/utils/index-usages-by-key.ts +28 -0
  45. package/src/core/diagnostics/utils/resolve-key-path.ts +5 -0
  46. package/src/core/discover-configs/discover-configs.ts +87 -0
  47. package/src/core/discover-configs/index.ts +1 -0
  48. package/src/core/discover-configs/is-intor-resolved-config.ts +15 -0
  49. package/src/core/extract-usages/README.md +84 -0
  50. package/src/core/extract-usages/collectors/collect-key-usages.ts +40 -0
  51. package/src/core/extract-usages/collectors/collect-pre-keys.ts +58 -0
  52. package/src/core/extract-usages/collectors/collect-replacement-usages.ts +68 -0
  53. package/src/core/extract-usages/collectors/collect-rich-usages.ts +57 -0
  54. package/src/core/extract-usages/collectors/collect-translator-bindings.ts +56 -0
  55. package/src/core/extract-usages/collectors/index.ts +5 -0
  56. package/src/core/extract-usages/collectors/utils/extract-static-object-keys.ts +31 -0
  57. package/src/core/extract-usages/collectors/utils/get-config-key.ts +15 -0
  58. package/src/core/extract-usages/collectors/utils/get-object-arg.ts +42 -0
  59. package/src/core/extract-usages/collectors/utils/is-static-string-literal.ts +26 -0
  60. package/src/core/extract-usages/collectors/utils/walk-translator-bindings.ts +47 -0
  61. package/src/core/extract-usages/collectors/utils/walk-translator-method-calls.ts +42 -0
  62. package/src/core/extract-usages/extract-usages.ts +88 -0
  63. package/src/core/extract-usages/index.ts +13 -0
  64. package/src/core/extract-usages/load-source-files-from-tscofnig.ts +76 -0
  65. package/src/core/extract-usages/translator-registry.ts +20 -0
  66. package/src/core/extract-usages/types.ts +53 -0
  67. package/src/core/index.ts +13 -0
  68. package/src/core/infer-schema/index.ts +3 -0
  69. package/src/core/infer-schema/infer-schemas.ts +16 -0
  70. package/src/core/infer-schema/messages/index.ts +1 -0
  71. package/src/core/infer-schema/messages/infer-messages-schema.ts +64 -0
  72. package/src/core/infer-schema/replacements/extract-interpolation-names.ts +70 -0
  73. package/src/core/infer-schema/replacements/index.ts +1 -0
  74. package/src/core/infer-schema/replacements/infer-replacements-schema.ts +63 -0
  75. package/src/core/infer-schema/rich/index.ts +1 -0
  76. package/src/core/infer-schema/rich/infer-rich-schema.ts +66 -0
  77. package/src/core/infer-schema/types.ts +42 -0
  78. package/src/core/infer-schema/utils/infer-object.ts +32 -0
  79. package/src/core/infer-schema/utils/is-message-object.ts +5 -0
  80. package/src/core/read-generated-schema.ts +42 -0
  81. package/src/core/scan-logger.ts +10 -0
  82. package/src/core/write-generated-files.ts +51 -0
  83. package/src/features/check/check.ts +75 -0
  84. package/src/features/check/index.ts +1 -0
  85. package/src/features/check/print-summary.ts +28 -0
  86. package/src/features/generate/generate.ts +76 -0
  87. package/src/features/generate/index.ts +1 -0
  88. package/src/features/generate/print-configs.ts +8 -0
  89. package/src/features/generate/print-summary.ts +19 -0
  90. package/src/features/index.ts +2 -0
  91. package/src/features/print-title.ts +7 -0
  92. package/src/features/spinner.ts +3 -0
@@ -0,0 +1,59 @@
1
+ import type { Diagnostic } from "./types";
2
+ import type { ExtractedUsages } from "../extract-usages";
3
+ import type { InferredSchemas } from "../infer-schema/types";
4
+ import { enforceMissingReplacements } from "./rules/enforce-missing-replacements";
5
+ import { enforceMissingRich } from "./rules/enforce-missing-rich";
6
+ import { keyEmpty, keyNotFound } from "./rules/key";
7
+ import {
8
+ replacementsNotAllowed,
9
+ replacementsMissing,
10
+ replacementsUnused,
11
+ } from "./rules/replacement";
12
+ import { richMissing, richNotAllowed, richUnused } from "./rules/rich";
13
+ import { indexUsagesByKey } from "./utils/index-usages-by-key";
14
+
15
+ export function collectDiagnostics(
16
+ { messagesSchema, replacementsSchema, richSchema }: InferredSchemas,
17
+ usages: ExtractedUsages,
18
+ ) {
19
+ const diagnostics: Diagnostic[] = [];
20
+
21
+ // Key
22
+ for (const usage of usages.keys) {
23
+ diagnostics.push(...keyNotFound(usage, messagesSchema), ...keyEmpty(usage));
24
+ }
25
+
26
+ // Replacements
27
+ for (const usage of usages.replacements) {
28
+ diagnostics.push(
29
+ ...replacementsNotAllowed(usage, replacementsSchema),
30
+ ...replacementsMissing(usage, replacementsSchema),
31
+ ...replacementsUnused(usage, replacementsSchema),
32
+ );
33
+ }
34
+
35
+ // Rich
36
+ for (const usage of usages.rich) {
37
+ diagnostics.push(
38
+ ...richNotAllowed(usage, richSchema),
39
+ ...richMissing(usage, richSchema),
40
+ ...richUnused(usage, richSchema),
41
+ );
42
+ }
43
+
44
+ // Ensure required replacements / rich tags are detected even when no usage provides them
45
+ const replacementIndex = indexUsagesByKey(usages.replacements);
46
+ const richIndex = indexUsagesByKey(usages.rich);
47
+ for (const usage of usages.keys) {
48
+ diagnostics.push(
49
+ ...enforceMissingReplacements(
50
+ usage,
51
+ replacementIndex,
52
+ replacementsSchema,
53
+ ),
54
+ ...enforceMissingRich(usage, richIndex, richSchema),
55
+ );
56
+ }
57
+
58
+ return diagnostics;
59
+ }
@@ -0,0 +1,41 @@
1
+ /* eslint-disable unicorn/no-array-sort */
2
+ import type { Diagnostic, DiagnosticGroup } from "./types";
3
+
4
+ export function groupDiagnostics(diagnostics: Diagnostic[]): DiagnosticGroup[] {
5
+ const map = new Map<string, DiagnosticGroup>();
6
+
7
+ for (const diagnostic of diagnostics) {
8
+ const { severity, method, message, messageKey, file, line, column } =
9
+ diagnostic;
10
+
11
+ const groupId = messageKey
12
+ ? `${file}::${messageKey}::${diagnostic.method}`
13
+ : `${file}::${line}:${column}`;
14
+
15
+ if (!map.has(groupId)) {
16
+ map.set(groupId, {
17
+ severity,
18
+ method,
19
+ messageKey,
20
+ problems: [],
21
+ file,
22
+ lines: [],
23
+ });
24
+ }
25
+ const group = map.get(groupId)!;
26
+
27
+ group.problems.push(message);
28
+ group.lines.push(line);
29
+
30
+ // severity upgrade (error > warn)
31
+ if (severity === "error") {
32
+ group.severity = "error";
33
+ }
34
+ }
35
+
36
+ for (const group of map.values()) {
37
+ group.lines = [...new Set(group.lines)].sort((a, b) => a - b);
38
+ }
39
+
40
+ return [...map.values()];
41
+ }
@@ -0,0 +1,2 @@
1
+ export { collectDiagnostics } from "./collect";
2
+ export { groupDiagnostics } from "./group";
@@ -0,0 +1,51 @@
1
+ export const DIAGNOSTIC_MESSAGES = {
2
+ // --------------------------------------------------
3
+ // Message key
4
+ // --------------------------------------------------
5
+ KEY_NOT_FOUND: {
6
+ code: "INTOR_KEY_NOT_FOUND",
7
+ message: () => "does not exist",
8
+ },
9
+
10
+ KEY_EMPTY: {
11
+ code: "INTOR_KEY_EMPTY",
12
+ message: () => "translation key cannot be empty",
13
+ },
14
+
15
+ // --------------------------------------------------
16
+ // Replacements
17
+ // --------------------------------------------------
18
+ REPLACEMENTS_NOT_ALLOWED: {
19
+ code: "INTOR_REPLACEMENTS_NOT_ALLOWED",
20
+ message: () => "does not accept replacements",
21
+ },
22
+
23
+ REPLACEMENTS_MISSING: {
24
+ code: "INTOR_REPLACEMENTS_MISSING",
25
+ message: (missing: string[]) =>
26
+ `missing replacements: ${missing.join(", ")}`,
27
+ },
28
+
29
+ REPLACEMENTS_UNUSED: {
30
+ code: "INTOR_REPLACEMENTS_UNUSED",
31
+ message: (extra: string[]) => `unused replacements: ${extra.join(", ")}`,
32
+ },
33
+
34
+ // --------------------------------------------------
35
+ // Rich tags
36
+ // --------------------------------------------------
37
+ RICH_NOT_ALLOWED: {
38
+ code: "INTOR_RICH_NOT_ALLOWED",
39
+ message: () => "does not accept rich tags",
40
+ },
41
+
42
+ RICH_MISSING: {
43
+ code: "INTOR_RICH_MISSING",
44
+ message: (missing: string[]) => `missing rich tags: ${missing.join(", ")}`,
45
+ },
46
+
47
+ RICH_UNUSED: {
48
+ code: "INTOR_RICH_UNUSED",
49
+ message: (extra: string[]) => `unused rich tags: ${extra.join(", ")}`,
50
+ },
51
+ } as const;
@@ -0,0 +1,55 @@
1
+ import type { KeyUsage, ReplacementUsage } from "../../extract-usages";
2
+ import type { InferNode } from "../../infer-schema";
3
+ import type { Diagnostic } from "../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../messages";
5
+ import { getSchemaNodeAtPath } from "../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../utils/resolve-key-path";
7
+
8
+ /**
9
+ * Detect missing replacements when no replacement usage exists anywhere.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // Expected:
14
+ * t("hello", { name })
15
+ *
16
+ * // Received:
17
+ * t("hello") // Replacement usage cannot be detected
18
+ * ```
19
+ */
20
+ export function enforceMissingReplacements(
21
+ usage: KeyUsage,
22
+ replacementIndex: Map<string, ReplacementUsage[]>,
23
+ replacementsSchema: InferNode,
24
+ ): Diagnostic[] {
25
+ const { method, key, preKey, file, line, column } = usage;
26
+ const diagnostics: Diagnostic[] = [];
27
+
28
+ const keyPath = resolveKeyPath(key, preKey);
29
+
30
+ // Replacements provided elsewhere
31
+ if (replacementIndex.has(`${usage.method}::${keyPath}`)) return diagnostics;
32
+
33
+ const schemaNode = getSchemaNodeAtPath(replacementsSchema, keyPath);
34
+
35
+ // No replacement schema defined
36
+ if (!schemaNode || schemaNode.kind !== "object") return diagnostics;
37
+
38
+ const expected: string[] = Object.keys(schemaNode.properties);
39
+
40
+ // No required replacements
41
+ if (expected.length === 0) return diagnostics;
42
+
43
+ diagnostics.push({
44
+ severity: "warn",
45
+ method,
46
+ messageKey: keyPath,
47
+ code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.code,
48
+ message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.message(expected),
49
+ file,
50
+ line,
51
+ column,
52
+ });
53
+
54
+ return diagnostics;
55
+ }
@@ -0,0 +1,55 @@
1
+ import type { KeyUsage, RichUsage } from "../../extract-usages";
2
+ import type { InferNode } from "../../infer-schema";
3
+ import type { Diagnostic } from "../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../messages";
5
+ import { getSchemaNodeAtPath } from "../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../utils/resolve-key-path";
7
+
8
+ /**
9
+ * Detect missing rich when no rich usage exists anywhere.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // Expected:
14
+ * tRich("hello", { link })
15
+ *
16
+ * // Received:
17
+ * tRich("hello") // Rich usage cannot be detected
18
+ * ```
19
+ */
20
+ export function enforceMissingRich(
21
+ usage: KeyUsage,
22
+ richIndex: Map<string, RichUsage[]>,
23
+ richSchema: InferNode,
24
+ ): Diagnostic[] {
25
+ const { method, key, preKey, file, line, column } = usage;
26
+ const diagnostics: Diagnostic[] = [];
27
+
28
+ const keyPath = resolveKeyPath(key, preKey);
29
+
30
+ // Rich tags provided elsewhere
31
+ if (richIndex.has(`${method}::${keyPath}`)) return diagnostics;
32
+
33
+ const schemaNode = getSchemaNodeAtPath(richSchema, keyPath);
34
+
35
+ // No rich schema defined
36
+ if (!schemaNode || schemaNode.kind !== "object") return diagnostics;
37
+
38
+ const expected: string[] = Object.keys(schemaNode.properties);
39
+
40
+ // No required rich tags
41
+ if (expected.length === 0) return diagnostics;
42
+
43
+ diagnostics.push({
44
+ severity: "warn",
45
+ method,
46
+ messageKey: keyPath,
47
+ code: DIAGNOSTIC_MESSAGES.RICH_MISSING.code,
48
+ message: DIAGNOSTIC_MESSAGES.RICH_MISSING.message(expected),
49
+ file,
50
+ line,
51
+ column,
52
+ });
53
+
54
+ return diagnostics;
55
+ }
@@ -0,0 +1,34 @@
1
+ import type { KeyUsage } from "../../../extract-usages";
2
+ import type { Diagnostic } from "../../types";
3
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
4
+
5
+ /**
6
+ * @example
7
+ * ```ts
8
+ * // Expected:
9
+ * t("hello")
10
+ *
11
+ * // Received:
12
+ * t("")
13
+ * ```
14
+ */
15
+ export function keyEmpty(usage: KeyUsage): Diagnostic[] {
16
+ const { method, key, file, line, column } = usage;
17
+
18
+ if (!key) {
19
+ return [
20
+ {
21
+ severity: "warn",
22
+ method,
23
+ messageKey: key,
24
+ code: DIAGNOSTIC_MESSAGES.KEY_EMPTY.code,
25
+ message: DIAGNOSTIC_MESSAGES.KEY_EMPTY.message(),
26
+ file,
27
+ line,
28
+ column,
29
+ },
30
+ ];
31
+ }
32
+
33
+ return [];
34
+ }
@@ -0,0 +1,2 @@
1
+ export { keyEmpty } from "./empty";
2
+ export { keyNotFound } from "./not-found";
@@ -0,0 +1,43 @@
1
+ import type { KeyUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * t("hello")
13
+ *
14
+ * // Received:
15
+ * t("missing")
16
+ * ```
17
+ */
18
+ export function keyNotFound(
19
+ usage: KeyUsage,
20
+ messagesSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ if (!key) return [];
25
+
26
+ const keyPath = resolveKeyPath(key, preKey);
27
+ if (!getSchemaNodeAtPath(messagesSchema, keyPath)) {
28
+ return [
29
+ {
30
+ severity: "warn",
31
+ method,
32
+ messageKey: key,
33
+ code: DIAGNOSTIC_MESSAGES.KEY_NOT_FOUND.code,
34
+ message: DIAGNOSTIC_MESSAGES.KEY_NOT_FOUND.message(),
35
+ file,
36
+ line,
37
+ column,
38
+ },
39
+ ];
40
+ }
41
+
42
+ return [];
43
+ }
@@ -0,0 +1,3 @@
1
+ export { replacementsNotAllowed } from "./not-allowed";
2
+ export { replacementsMissing } from "./missing";
3
+ export { replacementsUnused } from "./unused";
@@ -0,0 +1,48 @@
1
+ import type { ReplacementUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * t("hello", { name, phone })
13
+ *
14
+ * // Received:
15
+ * t("hello", { name })
16
+ * ```
17
+ */
18
+ export function replacementsMissing(
19
+ usage: ReplacementUsage,
20
+ replacementsSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ const keyPath = resolveKeyPath(key, preKey);
25
+ const schemaNode = getSchemaNodeAtPath(replacementsSchema, keyPath);
26
+ if (!schemaNode || schemaNode.kind !== "object") return [];
27
+
28
+ const expected = Object.keys(schemaNode.properties);
29
+ const actual = usage.replacements;
30
+ const missing = expected.filter((name) => !actual.includes(name));
31
+
32
+ if (missing.length > 0) {
33
+ return [
34
+ {
35
+ severity: "warn",
36
+ method,
37
+ messageKey: keyPath,
38
+ code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.code,
39
+ message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.message(missing),
40
+ file,
41
+ line,
42
+ column,
43
+ },
44
+ ];
45
+ }
46
+
47
+ return [];
48
+ }
@@ -0,0 +1,43 @@
1
+ import type { ReplacementUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * t("hello")
13
+ *
14
+ * // Received:
15
+ * t("hello", { name })
16
+ * ```
17
+ */
18
+ export function replacementsNotAllowed(
19
+ usage: ReplacementUsage,
20
+ replacementsSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ const keyPath = resolveKeyPath(key, preKey);
25
+ const schemaNode = getSchemaNodeAtPath(replacementsSchema, keyPath);
26
+
27
+ if (!schemaNode || schemaNode.kind !== "object") {
28
+ return [
29
+ {
30
+ severity: "warn",
31
+ method,
32
+ messageKey: keyPath,
33
+ code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_NOT_ALLOWED.code,
34
+ message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_NOT_ALLOWED.message(),
35
+ file,
36
+ line,
37
+ column,
38
+ },
39
+ ];
40
+ }
41
+
42
+ return [];
43
+ }
@@ -0,0 +1,48 @@
1
+ import type { ReplacementUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * t("hello", { name })
13
+ *
14
+ * // Received:
15
+ * t("hello", { name, phone })
16
+ * ```
17
+ */
18
+ export function replacementsUnused(
19
+ usage: ReplacementUsage,
20
+ replacementsSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ const keyPath = resolveKeyPath(key, preKey);
25
+ const schemaNode = getSchemaNodeAtPath(replacementsSchema, keyPath);
26
+ if (!schemaNode || schemaNode.kind !== "object") return [];
27
+
28
+ const expected = Object.keys(schemaNode.properties);
29
+ const actual = usage.replacements;
30
+ const extra = actual.filter((name) => !expected.includes(name));
31
+
32
+ if (extra.length > 0) {
33
+ return [
34
+ {
35
+ severity: "warn",
36
+ method,
37
+ messageKey: keyPath,
38
+ code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_UNUSED.code,
39
+ message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_UNUSED.message(extra),
40
+ file,
41
+ line,
42
+ column,
43
+ },
44
+ ];
45
+ }
46
+
47
+ return [];
48
+ }
@@ -0,0 +1,3 @@
1
+ export { richNotAllowed } from "./not-allowed";
2
+ export { richMissing } from "./missing";
3
+ export { richUnused } from "./unused";
@@ -0,0 +1,48 @@
1
+ import type { RichUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * tRich("hello", { link })
13
+ *
14
+ * // Received:
15
+ * tRich("hello", { link, b })
16
+ * ```
17
+ */
18
+ export function richMissing(
19
+ usage: RichUsage,
20
+ richSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ const keyPath = resolveKeyPath(key, preKey);
25
+ const schemaNode = getSchemaNodeAtPath(richSchema, keyPath);
26
+ if (!schemaNode || schemaNode.kind !== "object") return [];
27
+
28
+ const expected = Object.keys(schemaNode.properties);
29
+ const actual = usage.rich;
30
+ const missing = expected.filter((tag) => !actual.includes(tag));
31
+
32
+ if (missing.length > 0) {
33
+ return [
34
+ {
35
+ severity: "warn",
36
+ method,
37
+ messageKey: keyPath,
38
+ code: DIAGNOSTIC_MESSAGES.RICH_MISSING.code,
39
+ message: DIAGNOSTIC_MESSAGES.RICH_MISSING.message(missing),
40
+ file,
41
+ line,
42
+ column,
43
+ },
44
+ ];
45
+ }
46
+
47
+ return [];
48
+ }
@@ -0,0 +1,43 @@
1
+ import type { RichUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * tRich("hello")
13
+ *
14
+ * // Received:
15
+ * tRich("hello", { link })
16
+ * ```
17
+ */
18
+ export function richNotAllowed(
19
+ usage: RichUsage,
20
+ richSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ const keyPath = resolveKeyPath(key, preKey);
25
+ const schemaNode = getSchemaNodeAtPath(richSchema, keyPath);
26
+
27
+ if (!schemaNode || schemaNode.kind !== "object") {
28
+ return [
29
+ {
30
+ severity: "warn",
31
+ method,
32
+ messageKey: keyPath,
33
+ code: DIAGNOSTIC_MESSAGES.RICH_NOT_ALLOWED.code,
34
+ message: DIAGNOSTIC_MESSAGES.RICH_NOT_ALLOWED.message(),
35
+ file,
36
+ line,
37
+ column,
38
+ },
39
+ ];
40
+ }
41
+
42
+ return [];
43
+ }
@@ -0,0 +1,48 @@
1
+ import type { RichUsage } from "../../../extract-usages";
2
+ import type { InferNode } from "../../../infer-schema";
3
+ import type { Diagnostic } from "../../types";
4
+ import { DIAGNOSTIC_MESSAGES } from "../../messages";
5
+ import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
6
+ import { resolveKeyPath } from "../../utils/resolve-key-path";
7
+
8
+ /**
9
+ * @example
10
+ * ```ts
11
+ * // Expected:
12
+ * t("hello", { name })
13
+ *
14
+ * // Received:
15
+ * t("hello", { name, phone })
16
+ * ```
17
+ */
18
+ export function richUnused(
19
+ usage: RichUsage,
20
+ richSchema: InferNode,
21
+ ): Diagnostic[] {
22
+ const { method, key, preKey, file, line, column } = usage;
23
+
24
+ const keyPath = resolveKeyPath(key, preKey);
25
+ const schemaNode = getSchemaNodeAtPath(richSchema, keyPath);
26
+ if (!schemaNode || schemaNode.kind !== "object") return [];
27
+
28
+ const expected = Object.keys(schemaNode.properties);
29
+ const actual = usage.rich;
30
+ const extra = actual.filter((tag) => !expected.includes(tag));
31
+
32
+ if (extra.length > 0) {
33
+ return [
34
+ {
35
+ severity: "warn",
36
+ method,
37
+ messageKey: keyPath,
38
+ code: DIAGNOSTIC_MESSAGES.RICH_UNUSED.code,
39
+ message: DIAGNOSTIC_MESSAGES.RICH_UNUSED.message(extra),
40
+ file,
41
+ line,
42
+ column,
43
+ },
44
+ ];
45
+ }
46
+
47
+ return [];
48
+ }
@@ -0,0 +1,21 @@
1
+ import type { TranslatorMethod } from "../extract-usages";
2
+
3
+ export interface Diagnostic {
4
+ severity: "error" | "warn";
5
+ method: TranslatorMethod;
6
+ messageKey: string;
7
+ code: string;
8
+ message: string;
9
+ file: string;
10
+ line: number;
11
+ column: number;
12
+ }
13
+
14
+ export interface DiagnosticGroup {
15
+ severity: "error" | "warn";
16
+ method: TranslatorMethod;
17
+ messageKey: string;
18
+ problems: string[]; // list of bullet messages
19
+ file: string;
20
+ lines: number[]; // sorted, unique
21
+ }