intor-cli 0.0.1 → 0.0.2
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.
- package/package.json +1 -2
- package/src/core/diagnostics/collect.ts +11 -5
- package/src/core/diagnostics/group.ts +24 -4
- package/src/core/diagnostics/messages.ts +16 -8
- package/src/core/diagnostics/rules/pre-key/index.ts +1 -0
- package/src/core/diagnostics/rules/pre-key/not-found.ts +41 -0
- package/src/core/diagnostics/types.ts +5 -3
- package/src/core/discover-configs/discover-configs.ts +2 -1
- package/src/core/discover-configs/load-module.ts +3 -0
- package/src/core/extract-usages/collectors/collect-pre-keys.ts +52 -22
- package/src/core/extract-usages/extract-usages-from-source-file.ts +72 -0
- package/src/core/extract-usages/extract-usages.ts +22 -45
- package/src/core/extract-usages/index.ts +1 -0
- package/src/core/extract-usages/load-source-files-from-tscofnig.ts +5 -19
- package/src/core/extract-usages/types.ts +10 -2
- package/src/features/check/check.ts +9 -2
- package/src/features/check/dedupe-pre-key-usages.ts +25 -0
- package/src/features/check/print-summary.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "intor-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Intor CLI",
|
|
5
5
|
"author": "Yiming Liao",
|
|
6
6
|
"homepage": "https://github.com/yiming-liao/intor-cli#readme",
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"typescript-eslint": "8.51.0",
|
|
67
67
|
"vitest": "4.0.16"
|
|
68
68
|
},
|
|
69
|
-
"peerDependencies": {},
|
|
70
69
|
"publishConfig": {
|
|
71
70
|
"access": "public"
|
|
72
71
|
}
|
|
@@ -4,6 +4,7 @@ import type { InferredSchemas } from "../infer-schema/types";
|
|
|
4
4
|
import { enforceMissingReplacements } from "./rules/enforce-missing-replacements";
|
|
5
5
|
import { enforceMissingRich } from "./rules/enforce-missing-rich";
|
|
6
6
|
import { keyEmpty, keyNotFound } from "./rules/key";
|
|
7
|
+
import { preKeyNotFound } from "./rules/pre-key";
|
|
7
8
|
import {
|
|
8
9
|
replacementsNotAllowed,
|
|
9
10
|
replacementsMissing,
|
|
@@ -18,13 +19,18 @@ export function collectDiagnostics(
|
|
|
18
19
|
) {
|
|
19
20
|
const diagnostics: Diagnostic[] = [];
|
|
20
21
|
|
|
22
|
+
// PreKey
|
|
23
|
+
for (const usage of usages.preKey) {
|
|
24
|
+
diagnostics.push(...preKeyNotFound(usage, messagesSchema));
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
// Key
|
|
22
|
-
for (const usage of usages.
|
|
28
|
+
for (const usage of usages.key) {
|
|
23
29
|
diagnostics.push(...keyNotFound(usage, messagesSchema), ...keyEmpty(usage));
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
//
|
|
27
|
-
for (const usage of usages.
|
|
32
|
+
// Replacement
|
|
33
|
+
for (const usage of usages.replacement) {
|
|
28
34
|
diagnostics.push(
|
|
29
35
|
...replacementsNotAllowed(usage, replacementsSchema),
|
|
30
36
|
...replacementsMissing(usage, replacementsSchema),
|
|
@@ -42,9 +48,9 @@ export function collectDiagnostics(
|
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
// Ensure required replacements / rich tags are detected even when no usage provides them
|
|
45
|
-
const replacementIndex = indexUsagesByKey(usages.
|
|
51
|
+
const replacementIndex = indexUsagesByKey(usages.replacement);
|
|
46
52
|
const richIndex = indexUsagesByKey(usages.rich);
|
|
47
|
-
for (const usage of usages.
|
|
53
|
+
for (const usage of usages.key) {
|
|
48
54
|
diagnostics.push(
|
|
49
55
|
...enforceMissingReplacements(
|
|
50
56
|
usage,
|
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
/* eslint-disable unicorn/no-array-sort */
|
|
2
2
|
import type { Diagnostic, DiagnosticGroup } from "./types";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Group diagnostics (by file + messageKey + method).
|
|
6
|
+
*/
|
|
4
7
|
export function groupDiagnostics(diagnostics: Diagnostic[]): DiagnosticGroup[] {
|
|
5
8
|
const map = new Map<string, DiagnosticGroup>();
|
|
6
9
|
|
|
7
10
|
for (const diagnostic of diagnostics) {
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const {
|
|
12
|
+
severity,
|
|
13
|
+
factory,
|
|
14
|
+
method,
|
|
15
|
+
message,
|
|
16
|
+
messageKey,
|
|
17
|
+
file,
|
|
18
|
+
line,
|
|
19
|
+
column,
|
|
20
|
+
} = diagnostic;
|
|
21
|
+
|
|
22
|
+
// --------------------------------------------------
|
|
23
|
+
// Grouping key
|
|
24
|
+
// - Prefer semantic key (messageKey + method)
|
|
25
|
+
// - Fallback to exact source location
|
|
26
|
+
// --------------------------------------------------
|
|
11
27
|
const groupId = messageKey
|
|
12
28
|
? `${file}::${messageKey}::${diagnostic.method}`
|
|
13
29
|
: `${file}::${line}:${column}`;
|
|
14
30
|
|
|
31
|
+
// Initialize group if not exists
|
|
15
32
|
if (!map.has(groupId)) {
|
|
16
33
|
map.set(groupId, {
|
|
17
34
|
severity,
|
|
35
|
+
factory,
|
|
18
36
|
method,
|
|
19
37
|
messageKey,
|
|
20
38
|
problems: [],
|
|
@@ -24,15 +42,17 @@ export function groupDiagnostics(diagnostics: Diagnostic[]): DiagnosticGroup[] {
|
|
|
24
42
|
}
|
|
25
43
|
const group = map.get(groupId)!;
|
|
26
44
|
|
|
45
|
+
// Aggregate messages & lines
|
|
27
46
|
group.problems.push(message);
|
|
28
47
|
group.lines.push(line);
|
|
29
48
|
|
|
30
|
-
//
|
|
49
|
+
// Severity escalation (error > warn)
|
|
31
50
|
if (severity === "error") {
|
|
32
51
|
group.severity = "error";
|
|
33
52
|
}
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
// Normalize line numbers (unique + sorted)
|
|
36
56
|
for (const group of map.values()) {
|
|
37
57
|
group.lines = [...new Set(group.lines)].sort((a, b) => a - b);
|
|
38
58
|
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
export const DIAGNOSTIC_MESSAGES = {
|
|
2
|
+
// --------------------------------------------------
|
|
3
|
+
// PreKey
|
|
4
|
+
// --------------------------------------------------
|
|
5
|
+
PRE_KEY_NOT_FOUND: {
|
|
6
|
+
code: "INTOR_PRE_KEY_NOT_FOUND",
|
|
7
|
+
message: () => "preKey does not exist",
|
|
8
|
+
},
|
|
9
|
+
|
|
2
10
|
// --------------------------------------------------
|
|
3
11
|
// Message key
|
|
4
12
|
// --------------------------------------------------
|
|
5
13
|
KEY_NOT_FOUND: {
|
|
6
14
|
code: "INTOR_KEY_NOT_FOUND",
|
|
7
|
-
message: () => "does not exist",
|
|
15
|
+
message: () => "key does not exist",
|
|
8
16
|
},
|
|
9
17
|
|
|
10
18
|
KEY_EMPTY: {
|
|
11
19
|
code: "INTOR_KEY_EMPTY",
|
|
12
|
-
message: () => "
|
|
20
|
+
message: () => "key cannot be empty",
|
|
13
21
|
},
|
|
14
22
|
|
|
15
23
|
// --------------------------------------------------
|
|
@@ -17,18 +25,18 @@ export const DIAGNOSTIC_MESSAGES = {
|
|
|
17
25
|
// --------------------------------------------------
|
|
18
26
|
REPLACEMENTS_NOT_ALLOWED: {
|
|
19
27
|
code: "INTOR_REPLACEMENTS_NOT_ALLOWED",
|
|
20
|
-
message: () => "
|
|
28
|
+
message: () => "replacements are not allowed",
|
|
21
29
|
},
|
|
22
30
|
|
|
23
31
|
REPLACEMENTS_MISSING: {
|
|
24
32
|
code: "INTOR_REPLACEMENTS_MISSING",
|
|
25
33
|
message: (missing: string[]) =>
|
|
26
|
-
`missing
|
|
34
|
+
`replacements missing: ${missing.join(", ")}`,
|
|
27
35
|
},
|
|
28
36
|
|
|
29
37
|
REPLACEMENTS_UNUSED: {
|
|
30
38
|
code: "INTOR_REPLACEMENTS_UNUSED",
|
|
31
|
-
message: (extra: string[]) => `unused
|
|
39
|
+
message: (extra: string[]) => `replacements unused: ${extra.join(", ")}`,
|
|
32
40
|
},
|
|
33
41
|
|
|
34
42
|
// --------------------------------------------------
|
|
@@ -36,16 +44,16 @@ export const DIAGNOSTIC_MESSAGES = {
|
|
|
36
44
|
// --------------------------------------------------
|
|
37
45
|
RICH_NOT_ALLOWED: {
|
|
38
46
|
code: "INTOR_RICH_NOT_ALLOWED",
|
|
39
|
-
message: () => "
|
|
47
|
+
message: () => "rich tags are not allowed",
|
|
40
48
|
},
|
|
41
49
|
|
|
42
50
|
RICH_MISSING: {
|
|
43
51
|
code: "INTOR_RICH_MISSING",
|
|
44
|
-
message: (missing: string[]) => `
|
|
52
|
+
message: (missing: string[]) => `rich tags missing: ${missing.join(", ")}`,
|
|
45
53
|
},
|
|
46
54
|
|
|
47
55
|
RICH_UNUSED: {
|
|
48
56
|
code: "INTOR_RICH_UNUSED",
|
|
49
|
-
message: (extra: string[]) => `
|
|
57
|
+
message: (extra: string[]) => `rich tags unused: ${extra.join(", ")}`,
|
|
50
58
|
},
|
|
51
59
|
} as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { preKeyNotFound } from "./not-found";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PreKeyUsage } 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
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Expected:
|
|
11
|
+
* useTranslator("hello")
|
|
12
|
+
*
|
|
13
|
+
* // Received:
|
|
14
|
+
* useTranslator("missing")
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function preKeyNotFound(
|
|
18
|
+
usage: PreKeyUsage,
|
|
19
|
+
messagesSchema: InferNode,
|
|
20
|
+
): Diagnostic[] {
|
|
21
|
+
const { factory, preKey, file, line, column } = usage;
|
|
22
|
+
|
|
23
|
+
if (!preKey) return [];
|
|
24
|
+
|
|
25
|
+
if (!getSchemaNodeAtPath(messagesSchema, preKey)) {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
severity: "warn",
|
|
29
|
+
factory,
|
|
30
|
+
messageKey: preKey,
|
|
31
|
+
code: DIAGNOSTIC_MESSAGES.PRE_KEY_NOT_FOUND.code,
|
|
32
|
+
message: DIAGNOSTIC_MESSAGES.PRE_KEY_NOT_FOUND.message(),
|
|
33
|
+
file,
|
|
34
|
+
line,
|
|
35
|
+
column,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { TranslatorMethod } from "../extract-usages";
|
|
1
|
+
import type { TranslatorFactory, TranslatorMethod } from "../extract-usages";
|
|
2
2
|
|
|
3
3
|
export interface Diagnostic {
|
|
4
4
|
severity: "error" | "warn";
|
|
5
|
-
|
|
5
|
+
factory?: TranslatorFactory;
|
|
6
|
+
method?: TranslatorMethod;
|
|
6
7
|
messageKey: string;
|
|
7
8
|
code: string;
|
|
8
9
|
message: string;
|
|
@@ -13,7 +14,8 @@ export interface Diagnostic {
|
|
|
13
14
|
|
|
14
15
|
export interface DiagnosticGroup {
|
|
15
16
|
severity: "error" | "warn";
|
|
16
|
-
|
|
17
|
+
factory?: TranslatorFactory;
|
|
18
|
+
method?: TranslatorMethod;
|
|
17
19
|
messageKey: string;
|
|
18
20
|
problems: string[]; // list of bullet messages
|
|
19
21
|
file: string;
|
|
@@ -4,6 +4,7 @@ import fg from "fast-glob";
|
|
|
4
4
|
import { type IntorResolvedConfig } from "intor";
|
|
5
5
|
import { createLogger } from "../scan-logger";
|
|
6
6
|
import { isIntorResolvedConfig } from "./is-intor-resolved-config";
|
|
7
|
+
import { loadModule } from "./load-module";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_PATTERNS = ["**/*.{ts,js}"];
|
|
9
10
|
const DEFAULT_IGNORE = [
|
|
@@ -58,7 +59,7 @@ export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
|
|
|
58
59
|
// Dynamic import & export inspection
|
|
59
60
|
// ----------------------------------------------------------------------
|
|
60
61
|
try {
|
|
61
|
-
const moduleExports = await
|
|
62
|
+
const moduleExports = await loadModule(absPath);
|
|
62
63
|
let matched = false;
|
|
63
64
|
|
|
64
65
|
for (const module of Object.values(moduleExports)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PreKeyMap, TranslatorBindingMap } from "../types";
|
|
1
|
+
import type { PreKeyMap, PreKeyUsage, TranslatorBindingMap } from "../types";
|
|
2
2
|
import { SyntaxKind, type SourceFile } from "ts-morph";
|
|
3
3
|
import { getObjectArg } from "./utils/get-object-arg";
|
|
4
4
|
import { isStaticStringLiteral } from "./utils/is-static-string-literal";
|
|
@@ -6,6 +6,11 @@ import { walkTranslatorBindings } from "./utils/walk-translator-bindings";
|
|
|
6
6
|
|
|
7
7
|
const PREKEY_PROPERTY_NAME = "preKey";
|
|
8
8
|
|
|
9
|
+
export interface CollectPreKeysResult {
|
|
10
|
+
preKeyMap: PreKeyMap;
|
|
11
|
+
usages: PreKeyUsage[];
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Collect static preKey values associated with translator bindings
|
|
11
16
|
* within a single source file.
|
|
@@ -13,14 +18,19 @@ const PREKEY_PROPERTY_NAME = "preKey";
|
|
|
13
18
|
export function collectPreKeys(
|
|
14
19
|
sourceFile: SourceFile,
|
|
15
20
|
translatorBindingMap: TranslatorBindingMap,
|
|
16
|
-
):
|
|
17
|
-
const preKeyMap = new Map
|
|
21
|
+
): CollectPreKeysResult {
|
|
22
|
+
const preKeyMap: PreKeyMap = new Map();
|
|
23
|
+
const usages: PreKeyUsage[] = [];
|
|
18
24
|
|
|
19
25
|
walkTranslatorBindings(sourceFile, ({ call, binding }) => {
|
|
20
26
|
// Iterate over destructured translator binding elements (e.g. { t, hasKey, ... })
|
|
21
27
|
for (const el of binding.getElements()) {
|
|
22
28
|
const localName = el.getNameNode().getText(); // `t` from `const { t } = useTranslator`
|
|
23
29
|
if (!translatorBindingMap.has(localName)) continue;
|
|
30
|
+
const translatorUsage = translatorBindingMap.get(localName);
|
|
31
|
+
if (!translatorUsage) return;
|
|
32
|
+
|
|
33
|
+
let preKey: string | undefined;
|
|
24
34
|
|
|
25
35
|
// -----------------------------------------------------------------------
|
|
26
36
|
// Resolve static preKey from translator factory arguments
|
|
@@ -28,31 +38,51 @@ export function collectPreKeys(
|
|
|
28
38
|
// 1. From the first positional string argument (e.g. `useTranslator("preKey")`)
|
|
29
39
|
const firstArg = call.getArguments()[0];
|
|
30
40
|
if (isStaticStringLiteral(firstArg)) {
|
|
31
|
-
|
|
32
|
-
continue;
|
|
41
|
+
preKey = firstArg.getLiteralText();
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
// 2. From the last options object (e.g. `getTranslator(_, { preKey })`)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
if (!preKey) {
|
|
46
|
+
const lastArg = getObjectArg(call, "last");
|
|
47
|
+
if (!lastArg) continue;
|
|
48
|
+
|
|
49
|
+
// Extract the `preKey` property from the options object
|
|
50
|
+
const prop = lastArg.getProperty(PREKEY_PROPERTY_NAME);
|
|
51
|
+
if (!prop || !prop.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
52
|
+
|
|
53
|
+
// Only accept static string initializers as preKey values
|
|
54
|
+
const value = prop.getInitializer();
|
|
55
|
+
if (!value) continue;
|
|
56
|
+
if (
|
|
57
|
+
!value.isKind(SyntaxKind.StringLiteral) &&
|
|
58
|
+
!value.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
59
|
+
) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
preKey = value.getLiteralText();
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
if (!preKey) continue;
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------
|
|
68
|
+
// Record
|
|
69
|
+
// ---------------------------------------------------------------------
|
|
70
|
+
preKeyMap.set(localName, preKey);
|
|
71
|
+
|
|
72
|
+
// Resolve source location for diagnostics
|
|
73
|
+
const pos = sourceFile.getLineAndColumnAtPos(call.getStart());
|
|
74
|
+
|
|
75
|
+
usages.push({
|
|
76
|
+
factory: translatorUsage.factory,
|
|
77
|
+
configKey: translatorUsage.configKey,
|
|
78
|
+
localName,
|
|
79
|
+
preKey,
|
|
80
|
+
file: sourceFile.getFilePath(),
|
|
81
|
+
line: pos.line,
|
|
82
|
+
column: pos.column,
|
|
83
|
+
});
|
|
54
84
|
}
|
|
55
85
|
});
|
|
56
86
|
|
|
57
|
-
return preKeyMap;
|
|
87
|
+
return { preKeyMap, usages };
|
|
58
88
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExtractedUsages, PreKeyMap } from "./types";
|
|
2
|
+
import type { SourceFile } from "ts-morph";
|
|
3
|
+
import {
|
|
4
|
+
collectTranslatorBindings,
|
|
5
|
+
collectKeyUsages,
|
|
6
|
+
collectReplacementUsages,
|
|
7
|
+
collectRichUsages,
|
|
8
|
+
collectPreKeys,
|
|
9
|
+
} from "./collectors";
|
|
10
|
+
|
|
11
|
+
function attachPreKey<T extends { localName: string; preKey?: string }>(
|
|
12
|
+
usages: T[],
|
|
13
|
+
preKeyMap: PreKeyMap,
|
|
14
|
+
) {
|
|
15
|
+
for (const usage of usages) usage.preKey = preKeyMap.get(usage.localName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract all static translator usages from a single source file.
|
|
20
|
+
*
|
|
21
|
+
* This is a pure, file-level extractor suitable for editor extensions
|
|
22
|
+
* and incremental analysis.
|
|
23
|
+
*/
|
|
24
|
+
export function extractUsagesFromSourceFile(
|
|
25
|
+
sourceFile: SourceFile,
|
|
26
|
+
): ExtractedUsages {
|
|
27
|
+
// -----------------------------------------------------------------------
|
|
28
|
+
// Translator binding
|
|
29
|
+
// -----------------------------------------------------------------------
|
|
30
|
+
const translatorBindingMap = collectTranslatorBindings(sourceFile);
|
|
31
|
+
if (translatorBindingMap.size === 0) {
|
|
32
|
+
return { preKey: [], key: [], replacement: [], rich: [] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// -----------------------------------------------------------------------
|
|
36
|
+
// Key usages
|
|
37
|
+
// -----------------------------------------------------------------------
|
|
38
|
+
const keyUsages = collectKeyUsages(sourceFile, translatorBindingMap);
|
|
39
|
+
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
// Replacement usages
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
const replacementUsages = collectReplacementUsages(
|
|
44
|
+
sourceFile,
|
|
45
|
+
translatorBindingMap,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// -----------------------------------------------------------------------
|
|
49
|
+
// Rich usages
|
|
50
|
+
// -----------------------------------------------------------------------
|
|
51
|
+
const richUsages = collectRichUsages(sourceFile, translatorBindingMap);
|
|
52
|
+
|
|
53
|
+
// -----------------------------------------------------------------------
|
|
54
|
+
// PreKey values
|
|
55
|
+
// -----------------------------------------------------------------------
|
|
56
|
+
const { preKeyMap, usages } = collectPreKeys(
|
|
57
|
+
sourceFile,
|
|
58
|
+
translatorBindingMap,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Attach preKey to all usages that support it
|
|
62
|
+
attachPreKey(keyUsages, preKeyMap);
|
|
63
|
+
attachPreKey(replacementUsages, preKeyMap);
|
|
64
|
+
attachPreKey(richUsages, preKeyMap);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
preKey: usages,
|
|
68
|
+
key: keyUsages,
|
|
69
|
+
replacement: replacementUsages,
|
|
70
|
+
rich: richUsages,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type { ExtractedUsages } from "./types";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { createLogger } from "../scan-logger";
|
|
4
|
-
import {
|
|
5
|
-
collectTranslatorBindings,
|
|
6
|
-
collectKeyUsages,
|
|
7
|
-
collectReplacementUsages,
|
|
8
|
-
collectRichUsages,
|
|
9
|
-
collectPreKeys,
|
|
10
|
-
} from "./collectors";
|
|
4
|
+
import { extractUsagesFromSourceFile } from "./extract-usages-from-source-file";
|
|
11
5
|
import { loadSourceFilesFromTsconfig } from "./load-source-files-from-tscofnig";
|
|
12
6
|
|
|
7
|
+
/** Check whether a file-level extraction produced any meaningful usage */
|
|
8
|
+
const isEmpty = (u: ExtractedUsages) =>
|
|
9
|
+
u.key.length === 0 && u.replacement.length === 0 && u.rich.length === 0;
|
|
10
|
+
|
|
13
11
|
export interface ExtractUsagesOptions {
|
|
14
12
|
tsconfigPath?: string;
|
|
15
13
|
debug?: boolean;
|
|
@@ -23,11 +21,13 @@ export function extractUsages(options?: ExtractUsagesOptions): ExtractedUsages {
|
|
|
23
21
|
const log = createLogger(debug);
|
|
24
22
|
|
|
25
23
|
const result: ExtractedUsages = {
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
preKey: [],
|
|
25
|
+
key: [],
|
|
26
|
+
replacement: [],
|
|
28
27
|
rich: [],
|
|
29
28
|
};
|
|
30
29
|
|
|
30
|
+
// Debug counters
|
|
31
31
|
let scannedFiles = 0;
|
|
32
32
|
let matchedFiles = 0;
|
|
33
33
|
|
|
@@ -38,46 +38,23 @@ export function extractUsages(options?: ExtractUsagesOptions): ExtractedUsages {
|
|
|
38
38
|
scannedFiles++;
|
|
39
39
|
log("scan", sourceFile.getFilePath());
|
|
40
40
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// File-level extraction (pure analysis, no side effects)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const partial = extractUsagesFromSourceFile(sourceFile);
|
|
45
|
+
if (isEmpty(partial)) continue;
|
|
46
46
|
matchedFiles++;
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// -----------------------------------------------------------------------
|
|
56
|
-
const replacementUsages = collectReplacementUsages(
|
|
57
|
-
sourceFile,
|
|
58
|
-
translatorBindingMap,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// -----------------------------------------------------------------------
|
|
62
|
-
// Rich usages
|
|
63
|
-
// -----------------------------------------------------------------------
|
|
64
|
-
const richUsages = collectRichUsages(sourceFile, translatorBindingMap);
|
|
65
|
-
|
|
66
|
-
// -----------------------------------------------------------------------
|
|
67
|
-
// PreKey values
|
|
68
|
-
// -----------------------------------------------------------------------
|
|
69
|
-
const preKeyMap = collectPreKeys(sourceFile, translatorBindingMap);
|
|
70
|
-
|
|
71
|
-
// Attach preKey to all usages that support it
|
|
72
|
-
for (const usage of [...keyUsages, ...replacementUsages, ...richUsages]) {
|
|
73
|
-
usage.preKey = preKeyMap.get(usage.localName);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
result.keys.push(...keyUsages);
|
|
77
|
-
result.replacements.push(...replacementUsages);
|
|
78
|
-
result.rich.push(...richUsages);
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Merge file-level results into the project-level result
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
result.preKey.push(...partial.preKey);
|
|
52
|
+
result.key.push(...partial.key);
|
|
53
|
+
result.replacement.push(...partial.replacement);
|
|
54
|
+
result.rich.push(...partial.rich);
|
|
79
55
|
}
|
|
80
56
|
|
|
57
|
+
// Debug summary
|
|
81
58
|
if (debug) {
|
|
82
59
|
console.log(
|
|
83
60
|
pc.dim(` › Scanned ${scannedFiles} files, matched ${matchedFiles} \n`),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import pc from "picocolors";
|
|
4
3
|
import { Project, type SourceFile } from "ts-morph";
|
|
4
|
+
import { createLogger } from "../scan-logger";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Load source files from a tsconfig.
|
|
@@ -20,6 +20,7 @@ export function loadSourceFilesFromTsconfig(
|
|
|
20
20
|
tsconfigPath: string,
|
|
21
21
|
debug: boolean,
|
|
22
22
|
): SourceFile[] {
|
|
23
|
+
const log = createLogger(debug);
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// 1. Try loading source files directly from the given tsconfig
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -35,13 +36,7 @@ export function loadSourceFilesFromTsconfig(
|
|
|
35
36
|
const references: { path: string }[] = rawConfig.references ?? [];
|
|
36
37
|
|
|
37
38
|
if (references.length === 0) return [];
|
|
38
|
-
|
|
39
|
-
if (debug) {
|
|
40
|
-
console.log(
|
|
41
|
-
pc.dim(" (info)") +
|
|
42
|
-
pc.gray(" no source files found, following project references"),
|
|
43
|
-
);
|
|
44
|
-
}
|
|
39
|
+
log("info", `no source files found, following project references`);
|
|
45
40
|
|
|
46
41
|
// ---------------------------------------------------------------------------
|
|
47
42
|
// 3. Load source files from each referenced tsconfig
|
|
@@ -53,20 +48,11 @@ export function loadSourceFilesFromTsconfig(
|
|
|
53
48
|
|
|
54
49
|
// Skip missing referenced tsconfig files
|
|
55
50
|
if (!fs.existsSync(refPath)) {
|
|
56
|
-
|
|
57
|
-
console.log(
|
|
58
|
-
pc.dim(" (warn)") +
|
|
59
|
-
pc.gray(` referenced tsconfig not found: ${refPath}`),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
51
|
+
log("warn", `referenced tsconfig not found: ${refPath}`);
|
|
62
52
|
continue;
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
console.log(
|
|
67
|
-
pc.dim(" (ref)") + pc.gray(` ${path.relative(process.cwd(), refPath)}`),
|
|
68
|
-
);
|
|
69
|
-
}
|
|
55
|
+
log("ref ", `${path.relative(process.cwd(), refPath)}`);
|
|
70
56
|
|
|
71
57
|
const refProject = new Project({ tsConfigFilePath: refPath });
|
|
72
58
|
collected.push(...refProject.getSourceFiles());
|
|
@@ -22,6 +22,13 @@ interface SourceLocation {
|
|
|
22
22
|
column: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Static translation prreKey usage extracted from a source file. */
|
|
26
|
+
export interface PreKeyUsage
|
|
27
|
+
extends Omit<TranslatorBinding, "method">, SourceLocation {
|
|
28
|
+
localName: string;
|
|
29
|
+
preKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/** Static translation key usage extracted from a source file. */
|
|
26
33
|
export interface KeyUsage extends TranslatorBinding, SourceLocation {
|
|
27
34
|
localName: string; // local binding name (e.g. `t`, `hasKey`)
|
|
@@ -47,7 +54,8 @@ export interface RichUsage extends TranslatorBinding, SourceLocation {
|
|
|
47
54
|
|
|
48
55
|
/** Aggregated static translator usages extracted from a project. */
|
|
49
56
|
export interface ExtractedUsages {
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
preKey: PreKeyUsage[];
|
|
58
|
+
key: KeyUsage[];
|
|
59
|
+
replacement: ReplacementUsage[];
|
|
52
60
|
rich: RichUsage[];
|
|
53
61
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../../core";
|
|
10
10
|
import { printTitle } from "../../features/print-title";
|
|
11
11
|
import { spinner } from "../spinner";
|
|
12
|
+
import { dedupePreKeyUsages } from "./dedupe-pre-key-usages";
|
|
12
13
|
import { printSummary } from "./print-summary";
|
|
13
14
|
|
|
14
15
|
function resolveConfigKey(
|
|
@@ -50,10 +51,16 @@ export async function check(
|
|
|
50
51
|
|
|
51
52
|
// per-config usages
|
|
52
53
|
const scopedUsages = {
|
|
53
|
-
|
|
54
|
+
preKey: dedupePreKeyUsages(
|
|
55
|
+
usages.preKey.filter(
|
|
56
|
+
(u) =>
|
|
57
|
+
resolveConfigKey(u.configKey, defaultConfigKey) === configKey,
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
key: usages.key.filter(
|
|
54
61
|
(u) => resolveConfigKey(u.configKey, defaultConfigKey) === configKey,
|
|
55
62
|
),
|
|
56
|
-
|
|
63
|
+
replacement: usages.replacement.filter(
|
|
57
64
|
(u) => resolveConfigKey(u.configKey, defaultConfigKey) === configKey,
|
|
58
65
|
),
|
|
59
66
|
rich: usages.rich.filter(
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PreKeyUsage } from "../../core/extract-usages";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deduplicate preKey usages from the same translator factory call.
|
|
5
|
+
*
|
|
6
|
+
* A single factory call (e.g. `useTranslator("home")`) may expose
|
|
7
|
+
* multiple methods (`t`, `tRich`, ...).
|
|
8
|
+
*
|
|
9
|
+
* PreKey validation should happen once per factory call,
|
|
10
|
+
* not once per method, otherwise diagnostics would be duplicated.
|
|
11
|
+
*/
|
|
12
|
+
export function dedupePreKeyUsages(usages: PreKeyUsage[]): PreKeyUsage[] {
|
|
13
|
+
const seen = new Set<string>();
|
|
14
|
+
|
|
15
|
+
return usages.filter((u) => {
|
|
16
|
+
const sig = [u.factory, u.configKey, u.preKey, u.file, u.line].join("|");
|
|
17
|
+
|
|
18
|
+
if (seen.has(sig)) {
|
|
19
|
+
return false;
|
|
20
|
+
} else {
|
|
21
|
+
seen.add(sig);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -13,9 +13,9 @@ export function printSummary(configId: string, grouped: DiagnosticGroup[]) {
|
|
|
13
13
|
|
|
14
14
|
// Log problems
|
|
15
15
|
for (const group of grouped) {
|
|
16
|
-
const { method, messageKey, problems, file, lines } = group;
|
|
16
|
+
const { factory, method, messageKey, problems, file, lines } = group;
|
|
17
17
|
|
|
18
|
-
const header = `${messageKey} ${`(${method})`}\n`;
|
|
18
|
+
const header = `${messageKey} ${`(${method ?? factory})`}\n`;
|
|
19
19
|
|
|
20
20
|
const problemsLine = [
|
|
21
21
|
...problems.map((p) => pc.gray(` - ${p}`)),
|