@chaoswise/intl 3.1.2 → 3.1.3
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/bin/chaoswise-intl.js +7 -0
- package/bin/scripts/conf/default.js +16 -2
- package/bin/scripts/nozhcn.js +125 -17
- package/bin/scripts/nozhcnGuard.js +444 -0
- package/bin/scripts/util/findZhCnInFile.js +127 -6
- package/bin/scripts/util/fixI18nDefaultInFile.js +102 -4
- package/bin/scripts/util/fixZhCnInFile.js +290 -0
- package/package.json +1 -1
package/bin/chaoswise-intl.js
CHANGED
|
@@ -4,6 +4,7 @@ const runUpdate = require('./scripts/update');
|
|
|
4
4
|
const runInitConfig = require('./scripts/initConfig');
|
|
5
5
|
const runAddLocale = require('./scripts/addLocale');
|
|
6
6
|
const runNoZhCn = require('./scripts/nozhcn');
|
|
7
|
+
const runNoZhCnGuard = require('./scripts/nozhcnGuard');
|
|
7
8
|
const runVerify = require('./scripts/verify');
|
|
8
9
|
|
|
9
10
|
const SCRIPTS = ['intl', 'collect', 'update', 'nozhcn', 'addLocale', 'init', 'verify'];
|
|
@@ -45,9 +46,15 @@ if (script === 'verify') {
|
|
|
45
46
|
// chaoswise-intl nozhcn check → same as above
|
|
46
47
|
// chaoswise-intl nozhcn fix → auto-fix comments, report remaining issues
|
|
47
48
|
// chaoswise-intl nozhcn report → non-blocking scan, output JSON report
|
|
49
|
+
// chaoswise-intl nozhcn guard → repository-level zero-Chinese guard (supports --report/--config/...)
|
|
48
50
|
if (script === 'nozhcn') {
|
|
49
51
|
// The sub-mode is the argument after 'nozhcn', default to 'check'
|
|
50
52
|
const modeArg = args[scriptIndex + 1];
|
|
53
|
+
if (modeArg === 'guard') {
|
|
54
|
+
const guardArgs = args.slice(scriptIndex + 2);
|
|
55
|
+
runNoZhCnGuard(guardArgs);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
51
58
|
const validModes = ['check', 'fix', 'report'];
|
|
52
59
|
const mode = validModes.includes(modeArg) ? modeArg : 'check';
|
|
53
60
|
runNoZhCn(mode);
|
|
@@ -104,8 +104,9 @@ module.exports = function (excludes = []) {
|
|
|
104
104
|
// 'string' - 字符串字面量
|
|
105
105
|
// 'template' - 模板字符串
|
|
106
106
|
// 'jsx' - JSX 文本内容
|
|
107
|
+
// 'console' - console.* 日志中的中文(可自动清理)
|
|
107
108
|
// 'identifier' - 标识符名称(极少出现,需手动修复)
|
|
108
|
-
noZhCnCheckTypes: ['comment', 'string', 'template', 'jsx'],
|
|
109
|
+
noZhCnCheckTypes: ['comment', 'string', 'template', 'jsx', 'console'],
|
|
109
110
|
|
|
110
111
|
// 注释中文修复策略(fix 模式生效):
|
|
111
112
|
// 'clean' - 仅删除中文字符,保留非中文内容;若注释全为中文则整条删除
|
|
@@ -137,12 +138,25 @@ module.exports = function (excludes = []) {
|
|
|
137
138
|
// 'empty' - 保留元素但清空内容:<title>搜索</title> → <title></title>
|
|
138
139
|
noZhCnSvgMetadataStrategy: 'remove',
|
|
139
140
|
|
|
141
|
+
// JSX 内联 <svg> 的元数据元素(<title>、<desc>、<metadata>)修复策略(fix 模式生效):
|
|
142
|
+
// 'remove' - 删除整个元素
|
|
143
|
+
// 'empty' - 保留元素但清空内容
|
|
144
|
+
// false - 不自动修复 JSX 内联 SVG metadata(仅报告)
|
|
145
|
+
// undefined/null - 跟随 noZhCnSvgMetadataStrategy(向后兼容)
|
|
146
|
+
noZhCnJsxSvgMetadataStrategy: null,
|
|
147
|
+
|
|
140
148
|
// SVG 属性中文修复策略(fix 模式生效):
|
|
141
149
|
// 'clean' - 删除属性值中的中文字符(及 CJK 标点),清理后为空则移除整个属性;
|
|
142
150
|
// 若修改了 id 属性,同步更新 SVG 内部引用(url(#…), href)
|
|
143
151
|
// 'remove' - 移除包含中文的属性
|
|
144
152
|
// false - 不自动修复属性中的中文
|
|
145
|
-
noZhCnSvgAttrStrategy: 'clean',
|
|
153
|
+
noZhCnSvgAttrStrategy: 'clean',
|
|
154
|
+
|
|
155
|
+
// console 中文日志修复策略(fix 模式生效):
|
|
156
|
+
// 'remove' - 删除包含中文的 console 语句(仅处理可安全定位的表达式语句)
|
|
157
|
+
// false - 不自动修复 console 中文日志(仅报告)
|
|
158
|
+
noZhCnConsoleStrategy: 'remove',
|
|
159
|
+
};
|
|
146
160
|
|
|
147
161
|
excludes.forEach((key) => delete config[key]);
|
|
148
162
|
|
package/bin/scripts/nozhcn.js
CHANGED
|
@@ -26,6 +26,9 @@ const getConf = require('./conf');
|
|
|
26
26
|
const { targetEntryFiles } = require('./util/getTargetFiles');
|
|
27
27
|
const findZhCnInFile = require('./util/findZhCnInFile');
|
|
28
28
|
const fixZhCnInFile = require('./util/fixZhCnInFile');
|
|
29
|
+
const { fixJsxSvgAttrInFile } = require('./util/fixZhCnInFile');
|
|
30
|
+
const { fixJsxSvgMetadataInFile } = require('./util/fixZhCnInFile');
|
|
31
|
+
const { fixConsoleZhCnInFile } = require('./util/fixZhCnInFile');
|
|
29
32
|
const fixI18nDefaultInFile = require('./util/fixI18nDefaultInFile');
|
|
30
33
|
const findZhCnInSvgFile = require('./util/findZhCnInSvgFile');
|
|
31
34
|
const fixZhCnInSvgFile = require('./util/fixZhCnInSvgFile');
|
|
@@ -35,12 +38,15 @@ const file = require('./util/file');
|
|
|
35
38
|
// ─── Display labels ──────────────────────────────────────────────────────────
|
|
36
39
|
|
|
37
40
|
const TYPE_LABEL = {
|
|
38
|
-
comment:
|
|
39
|
-
string:
|
|
40
|
-
template:
|
|
41
|
-
jsx:
|
|
42
|
-
'jsx-svg':
|
|
43
|
-
|
|
41
|
+
comment: 'Comment ',
|
|
42
|
+
string: 'String ',
|
|
43
|
+
template: 'Template ',
|
|
44
|
+
jsx: 'JSX Text ',
|
|
45
|
+
'jsx-svg': 'JSX SVG ',
|
|
46
|
+
'jsx-svg-attr': 'JSX SVG Attr',
|
|
47
|
+
'jsx-svg-metadata': 'JSX SVG Meta',
|
|
48
|
+
console: 'Console Log ',
|
|
49
|
+
identifier: 'Identifier ',
|
|
44
50
|
'svg-comment': 'SVG Comment ',
|
|
45
51
|
'svg-metadata': 'SVG Metadata',
|
|
46
52
|
'svg-text': 'SVG Text ',
|
|
@@ -53,6 +59,9 @@ const TYPE_COLOR = {
|
|
|
53
59
|
template: chalk.yellow,
|
|
54
60
|
jsx: chalk.cyan,
|
|
55
61
|
'jsx-svg': chalk.magenta,
|
|
62
|
+
'jsx-svg-attr': chalk.magenta,
|
|
63
|
+
'jsx-svg-metadata': chalk.gray,
|
|
64
|
+
console: chalk.blue,
|
|
56
65
|
identifier: chalk.red,
|
|
57
66
|
'svg-comment': chalk.gray,
|
|
58
67
|
'svg-metadata': chalk.gray,
|
|
@@ -61,8 +70,18 @@ const TYPE_COLOR = {
|
|
|
61
70
|
};
|
|
62
71
|
|
|
63
72
|
// Types that can be auto-fixed by nozhcn fix
|
|
64
|
-
// Note:
|
|
65
|
-
|
|
73
|
+
// Note:
|
|
74
|
+
// - 'svg-attr' / 'jsx-svg-attr' are conditionally fixable when noZhCnSvgAttrStrategy is set
|
|
75
|
+
// - 'svg-metadata' / 'jsx-svg-metadata' are conditionally fixable when noZhCnSvgMetadataStrategy is set
|
|
76
|
+
const AUTO_FIXABLE_TYPES = new Set([
|
|
77
|
+
'comment',
|
|
78
|
+
'svg-comment',
|
|
79
|
+
'svg-metadata',
|
|
80
|
+
'svg-attr',
|
|
81
|
+
'jsx-svg-attr',
|
|
82
|
+
'jsx-svg-metadata',
|
|
83
|
+
'console',
|
|
84
|
+
]);
|
|
66
85
|
|
|
67
86
|
// Types that `collect` script can handle
|
|
68
87
|
const COLLECT_TYPES = new Set(['string', 'template', 'jsx']);
|
|
@@ -163,13 +182,23 @@ function printSummary(findings, mode, autoFixedCount, i18nDefaultFixed) {
|
|
|
163
182
|
if (svgManualNeeded) {
|
|
164
183
|
console.log(
|
|
165
184
|
chalk.dim(
|
|
166
|
-
' Tip: SVG text
|
|
167
|
-
' For standalone .svg files, run fix to clean comments/metadata;\n' +
|
|
185
|
+
' Tip: SVG visible text occurrences require manual handling.\n' +
|
|
186
|
+
' For standalone .svg files, run fix to clean comments/metadata/attrs;\n' +
|
|
168
187
|
' for <text> content, consider replacing with a JS-driven label.'
|
|
169
188
|
)
|
|
170
189
|
);
|
|
171
190
|
}
|
|
172
191
|
|
|
192
|
+
const jsxSvgAttrNeeded = findings.some((f) => f.type === 'jsx-svg-attr');
|
|
193
|
+
if (jsxSvgAttrNeeded) {
|
|
194
|
+
console.log(
|
|
195
|
+
chalk.dim(
|
|
196
|
+
" Tip: JSX SVG attribute values (e.g. id=\"编组1\") are design-tool layer name artifacts.\n" +
|
|
197
|
+
" Run 'chaoswise-intl nozhcn fix' to auto-strip Chinese characters from them."
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
173
202
|
const identifierFound = findings.some((f) => f.type === 'identifier');
|
|
174
203
|
if (identifierFound) {
|
|
175
204
|
console.log(
|
|
@@ -201,7 +230,9 @@ module.exports = function noZhCn(mode) {
|
|
|
201
230
|
noZhCnDefaultLang = null,
|
|
202
231
|
noZhCnIncludeSvg = false,
|
|
203
232
|
noZhCnSvgMetadataStrategy = 'remove',
|
|
233
|
+
noZhCnJsxSvgMetadataStrategy = null,
|
|
204
234
|
noZhCnSvgAttrStrategy = 'clean',
|
|
235
|
+
noZhCnConsoleStrategy = 'remove',
|
|
205
236
|
localeOutput = 'src/public',
|
|
206
237
|
primaryRegx,
|
|
207
238
|
babelPresets,
|
|
@@ -285,9 +316,9 @@ module.exports = function noZhCn(mode) {
|
|
|
285
316
|
|
|
286
317
|
// ── Phase 2: Auto-fix (fix mode only) ────────────────────────────────────
|
|
287
318
|
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
319
|
+
// Most fix sub-phases run without full-project intermediate re-scans.
|
|
320
|
+
// For offset-based JS/TS fixers (console / jsx-svg-attr / jsx-svg-metadata),
|
|
321
|
+
// we re-scan only affected files right before patching to avoid stale ranges.
|
|
291
322
|
|
|
292
323
|
let autoFixedCount = 0;
|
|
293
324
|
let i18nDefaultFixed = 0;
|
|
@@ -296,6 +327,15 @@ module.exports = function noZhCn(mode) {
|
|
|
296
327
|
|
|
297
328
|
if (mode === 'fix') {
|
|
298
329
|
let anyFixApplied = false;
|
|
330
|
+
function getLatestFindingsByType(filePath, type) {
|
|
331
|
+
const result = findZhCnInFile(filePath, scanOpts);
|
|
332
|
+
if (result.error) {
|
|
333
|
+
errorFiles.push({ filePath, error: result.error });
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
return result.findings.filter((f) => f.type === type);
|
|
337
|
+
}
|
|
338
|
+
|
|
299
339
|
// ── Phase 2a: Fix comments in JS/TS files ─────────────────────────────
|
|
300
340
|
const commentByFile = new Map();
|
|
301
341
|
allFindings.forEach((f) => {
|
|
@@ -309,6 +349,24 @@ module.exports = function noZhCn(mode) {
|
|
|
309
349
|
});
|
|
310
350
|
if (autoFixedCount > 0) anyFixApplied = true;
|
|
311
351
|
|
|
352
|
+
// ── Phase 2a2: Remove Chinese console logs in JS/TS files ───────────
|
|
353
|
+
if (noZhCnConsoleStrategy) {
|
|
354
|
+
const consoleFiles = new Set();
|
|
355
|
+
allFindings.forEach((f) => {
|
|
356
|
+
if (f.type !== 'console') return;
|
|
357
|
+
consoleFiles.add(f.filePath);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
let consoleFixedCount = 0;
|
|
361
|
+
consoleFiles.forEach((fp) => {
|
|
362
|
+
const findings = getLatestFindingsByType(fp, 'console');
|
|
363
|
+
if (!findings.length) return;
|
|
364
|
+
consoleFixedCount += fixConsoleZhCnInFile(fp, findings, noZhCnConsoleStrategy);
|
|
365
|
+
});
|
|
366
|
+
autoFixedCount += consoleFixedCount;
|
|
367
|
+
if (consoleFixedCount > 0) anyFixApplied = true;
|
|
368
|
+
}
|
|
369
|
+
|
|
312
370
|
// ── Phase 2b: Replace .d('Chinese') with translations ─────────────────
|
|
313
371
|
if (noZhCnDefaultLang) {
|
|
314
372
|
const localePath = path.resolve(
|
|
@@ -333,10 +391,13 @@ module.exports = function noZhCn(mode) {
|
|
|
333
391
|
|
|
334
392
|
if (localeMap) {
|
|
335
393
|
const fixOpts = { i18nObject, i18nMethod, i18nDefaultFunctionKey, babelPresets, babelPlugins };
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
394
|
+
// Use ALL scanned files rather than only those with findings.
|
|
395
|
+
// When noZhCnIgnoreI18nDefault:true (the default), the initial Phase-1 scan
|
|
396
|
+
// blanks out .d('中文') before checking, so files whose ONLY Chinese text is
|
|
397
|
+
// inside .d() calls produce zero findings and would be silently skipped.
|
|
398
|
+
// fixI18nDefaultInFile has a cheap fast-path (no Chinese → early return),
|
|
399
|
+
// so scanning all files here causes no meaningful performance cost.
|
|
400
|
+
const filesToFix = new Set(files.map(({ filePath: fp }) => fp));
|
|
340
401
|
filesToFix.forEach((fp) => {
|
|
341
402
|
const result = fixI18nDefaultInFile(fp, localeMap, fixOpts);
|
|
342
403
|
i18nDefaultFixed += result.fixed;
|
|
@@ -380,6 +441,53 @@ module.exports = function noZhCn(mode) {
|
|
|
380
441
|
if (svgFixedCount > 0) anyFixApplied = true;
|
|
381
442
|
}
|
|
382
443
|
|
|
444
|
+
// ── Phase 2d: Fix JSX SVG attribute strings in JS/TS files ───────────
|
|
445
|
+
// e.g. <svg id="编组1"> → <svg id="1"> (strip Chinese layer-name artifacts)
|
|
446
|
+
if (noZhCnSvgAttrStrategy) {
|
|
447
|
+
const jsxSvgAttrFiles = new Set();
|
|
448
|
+
allFindings.forEach((f) => {
|
|
449
|
+
if (f.type !== 'jsx-svg-attr') return;
|
|
450
|
+
jsxSvgAttrFiles.add(f.filePath);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
jsxSvgAttrFiles.forEach((fp) => {
|
|
454
|
+
const findings = getLatestFindingsByType(fp, 'jsx-svg-attr');
|
|
455
|
+
if (!findings.length) return;
|
|
456
|
+
const fixed = fixJsxSvgAttrInFile(fp, findings, noZhCnSvgAttrStrategy);
|
|
457
|
+
svgFixedCount += fixed;
|
|
458
|
+
});
|
|
459
|
+
if (svgFixedCount > 0) anyFixApplied = true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Phase 2e: Fix JSX SVG metadata text in JS/TS files ───────────────
|
|
463
|
+
// e.g. <title>添加icon</title> (inside JSX <svg>)
|
|
464
|
+
// Strategy precedence:
|
|
465
|
+
// 1) noZhCnJsxSvgMetadataStrategy (if explicitly set, including false)
|
|
466
|
+
// 2) fallback to noZhCnSvgMetadataStrategy (backward-compat)
|
|
467
|
+
const hasExplicitJsxMetaStrategy = Object.prototype.hasOwnProperty.call(
|
|
468
|
+
conf,
|
|
469
|
+
'noZhCnJsxSvgMetadataStrategy'
|
|
470
|
+
);
|
|
471
|
+
const jsxMetaStrategy = hasExplicitJsxMetaStrategy
|
|
472
|
+
? noZhCnJsxSvgMetadataStrategy
|
|
473
|
+
: noZhCnSvgMetadataStrategy;
|
|
474
|
+
|
|
475
|
+
if (jsxMetaStrategy) {
|
|
476
|
+
const jsxSvgMetadataFiles = new Set();
|
|
477
|
+
allFindings.forEach((f) => {
|
|
478
|
+
if (f.type !== 'jsx-svg-metadata') return;
|
|
479
|
+
jsxSvgMetadataFiles.add(f.filePath);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
jsxSvgMetadataFiles.forEach((fp) => {
|
|
483
|
+
const findings = getLatestFindingsByType(fp, 'jsx-svg-metadata');
|
|
484
|
+
if (!findings.length) return;
|
|
485
|
+
const fixed = fixJsxSvgMetadataInFile(fp, findings, jsxMetaStrategy);
|
|
486
|
+
svgFixedCount += fixed;
|
|
487
|
+
});
|
|
488
|
+
if (svgFixedCount > 0) anyFixApplied = true;
|
|
489
|
+
}
|
|
490
|
+
|
|
383
491
|
// ── Single final re-scan after all fixes ──────────────────────────────
|
|
384
492
|
if (anyFixApplied) {
|
|
385
493
|
allFindings = [];
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ROOT = process.cwd();
|
|
5
|
+
const DEFAULT_CONFIG_FILE = '.nozhcn-guard.json';
|
|
6
|
+
const CJK_REGEXP = /[\u3400-\u9FFF]/g;
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
include: ['.'],
|
|
10
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.svg', '.html', '.yml', '.yaml'],
|
|
11
|
+
scanAllFiles: true,
|
|
12
|
+
skipBinaryFiles: true,
|
|
13
|
+
exclude: [
|
|
14
|
+
'**/.git/**',
|
|
15
|
+
'**/node_modules/**',
|
|
16
|
+
'**/portalWeb/**',
|
|
17
|
+
'**/publish/**',
|
|
18
|
+
'**/helpdesk-dashboard/**',
|
|
19
|
+
'**/.kiro/**',
|
|
20
|
+
'**/memories/**',
|
|
21
|
+
],
|
|
22
|
+
allowChinesePaths: [
|
|
23
|
+
'docs/**',
|
|
24
|
+
'locales/**',
|
|
25
|
+
'src/locales/**',
|
|
26
|
+
'portalWeb.json',
|
|
27
|
+
'NOZHCN.md',
|
|
28
|
+
],
|
|
29
|
+
ignoreI18nDefault: false,
|
|
30
|
+
maxPrintPerFile: 20,
|
|
31
|
+
reportFile: 'nozhcn-guard-report.json',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function normalizePath(filePath) {
|
|
35
|
+
return filePath.split(path.sep).join('/').replace(/^\.\//, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasGlob(pattern) {
|
|
39
|
+
return /[*?]/.test(pattern);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeRegExp(text) {
|
|
43
|
+
return text.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function globToRegExp(glob) {
|
|
47
|
+
const normalized = normalizePath(glob);
|
|
48
|
+
if (normalized.startsWith('**/')) {
|
|
49
|
+
const rest = normalized.slice(3);
|
|
50
|
+
const restRegexp = globToRegExp(rest).source.replace(/^\^/, '').replace(/\$$/, '');
|
|
51
|
+
return new RegExp(`^(?:.*/)?${restRegexp}$`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let source = '';
|
|
55
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
56
|
+
const ch = normalized[i];
|
|
57
|
+
const next = normalized[i + 1];
|
|
58
|
+
if (ch === '*') {
|
|
59
|
+
if (next === '*') {
|
|
60
|
+
source += '.*';
|
|
61
|
+
i += 1;
|
|
62
|
+
} else {
|
|
63
|
+
source += '[^/]*';
|
|
64
|
+
}
|
|
65
|
+
} else if (ch === '?') {
|
|
66
|
+
source += '[^/]';
|
|
67
|
+
} else {
|
|
68
|
+
source += escapeRegExp(ch);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new RegExp(`^${source}$`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadConfig(configFile) {
|
|
76
|
+
const configPath = path.resolve(ROOT, configFile || DEFAULT_CONFIG_FILE);
|
|
77
|
+
if (!fs.existsSync(configPath)) {
|
|
78
|
+
return { configPath, config: { ...DEFAULT_CONFIG } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
82
|
+
const userConfig = JSON.parse(raw);
|
|
83
|
+
const config = {
|
|
84
|
+
...DEFAULT_CONFIG,
|
|
85
|
+
...userConfig,
|
|
86
|
+
include: Array.isArray(userConfig.include) ? userConfig.include : DEFAULT_CONFIG.include,
|
|
87
|
+
extensions: Array.isArray(userConfig.extensions) ? userConfig.extensions : DEFAULT_CONFIG.extensions,
|
|
88
|
+
exclude: Array.isArray(userConfig.exclude) ? userConfig.exclude : DEFAULT_CONFIG.exclude,
|
|
89
|
+
allowChinesePaths: Array.isArray(userConfig.allowChinesePaths)
|
|
90
|
+
? userConfig.allowChinesePaths
|
|
91
|
+
: DEFAULT_CONFIG.allowChinesePaths,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return { configPath, config };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildMatchers(patterns) {
|
|
98
|
+
const regexps = [];
|
|
99
|
+
const literals = new Set();
|
|
100
|
+
|
|
101
|
+
for (const pattern of patterns) {
|
|
102
|
+
if (hasGlob(pattern)) {
|
|
103
|
+
regexps.push(globToRegExp(pattern));
|
|
104
|
+
} else {
|
|
105
|
+
literals.add(normalizePath(pattern));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { regexps, literals };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchPath(relativePath, matchers) {
|
|
113
|
+
const normalized = normalizePath(relativePath);
|
|
114
|
+
if (matchers.literals.has(normalized)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const regexp of matchers.regexps) {
|
|
119
|
+
if (regexp.test(normalized)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldScanFile(relativePath, config, excludeMatchers, allowMatchers) {
|
|
128
|
+
if (matchPath(relativePath, excludeMatchers)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (matchPath(relativePath, allowMatchers)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (config.scanAllFiles) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
139
|
+
return config.extensions.includes(ext);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isLikelyBinaryBuffer(buffer) {
|
|
143
|
+
if (!buffer || buffer.length === 0) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
(buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) ||
|
|
149
|
+
(buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) ||
|
|
150
|
+
(buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff)
|
|
151
|
+
) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sampleLength = Math.min(buffer.length, 8000);
|
|
156
|
+
let suspicious = 0;
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < sampleLength; i += 1) {
|
|
159
|
+
const byte = buffer[i];
|
|
160
|
+
if (byte === 0) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isTextControl = byte === 9 || byte === 10 || byte === 13;
|
|
165
|
+
const isPrintableAscii = byte >= 32 && byte <= 126;
|
|
166
|
+
const isHighByte = byte >= 128;
|
|
167
|
+
if (!isTextControl && !isPrintableAscii && !isHighByte) {
|
|
168
|
+
suspicious += 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return suspicious / sampleLength > 0.3;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function decodeUtf16LE(buffer, offset) {
|
|
176
|
+
const chars = [];
|
|
177
|
+
for (let i = offset || 0; i + 1 < buffer.length; i += 2) {
|
|
178
|
+
chars.push(String.fromCharCode(buffer[i] | (buffer[i + 1] << 8)));
|
|
179
|
+
}
|
|
180
|
+
return chars.join('');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function decodeUtf16BE(buffer, offset) {
|
|
184
|
+
const chars = [];
|
|
185
|
+
for (let i = offset || 0; i + 1 < buffer.length; i += 2) {
|
|
186
|
+
chars.push(String.fromCharCode((buffer[i] << 8) | buffer[i + 1]));
|
|
187
|
+
}
|
|
188
|
+
return chars.join('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function looksLikeUtf16WithoutBom(buffer) {
|
|
192
|
+
const sampleLength = Math.min(buffer.length, 8000);
|
|
193
|
+
if (sampleLength < 4) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let evenZero = 0;
|
|
198
|
+
let oddZero = 0;
|
|
199
|
+
let pairs = 0;
|
|
200
|
+
for (let i = 0; i + 1 < sampleLength; i += 2) {
|
|
201
|
+
if (buffer[i] === 0) {
|
|
202
|
+
evenZero += 1;
|
|
203
|
+
}
|
|
204
|
+
if (buffer[i + 1] === 0) {
|
|
205
|
+
oddZero += 1;
|
|
206
|
+
}
|
|
207
|
+
pairs += 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (pairs === 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const evenRatio = evenZero / pairs;
|
|
215
|
+
const oddRatio = oddZero / pairs;
|
|
216
|
+
|
|
217
|
+
if (oddRatio > 0.35 && oddRatio > evenRatio * 1.5) {
|
|
218
|
+
return 'utf16le';
|
|
219
|
+
}
|
|
220
|
+
if (evenRatio > 0.35 && evenRatio > oddRatio * 1.5) {
|
|
221
|
+
return 'utf16be';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function decodeTextBuffer(buffer) {
|
|
228
|
+
if (!buffer || buffer.length === 0) {
|
|
229
|
+
return { text: '', isBinary: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
233
|
+
return { text: buffer.toString('utf8', 3), isBinary: false };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
237
|
+
return { text: decodeUtf16LE(buffer, 2), isBinary: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
241
|
+
return { text: decodeUtf16BE(buffer, 2), isBinary: false };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const inferredUtf16 = looksLikeUtf16WithoutBom(buffer);
|
|
245
|
+
if (inferredUtf16 === 'utf16le') {
|
|
246
|
+
return { text: decodeUtf16LE(buffer, 0), isBinary: false };
|
|
247
|
+
}
|
|
248
|
+
if (inferredUtf16 === 'utf16be') {
|
|
249
|
+
return { text: decodeUtf16BE(buffer, 0), isBinary: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (isLikelyBinaryBuffer(buffer)) {
|
|
253
|
+
return { text: '', isBinary: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { text: buffer.toString('utf8'), isBinary: false };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function eraseRangeWithSpaces(input, start, end) {
|
|
260
|
+
let output = '';
|
|
261
|
+
for (let i = start; i < end; i += 1) {
|
|
262
|
+
output += input[i] === '\n' ? '\n' : ' ';
|
|
263
|
+
}
|
|
264
|
+
return output;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function blankI18nDefaultText(content) {
|
|
268
|
+
const dCallRegexp = /\.d\(\s*(?:'[^'\\]*(?:\\.[^'\\]*)*'|"[^"\\]*(?:\\.[^"\\]*)*"|`[\s\S]*?`)\s*\)/g;
|
|
269
|
+
let transformed = '';
|
|
270
|
+
let cursor = 0;
|
|
271
|
+
let match = dCallRegexp.exec(content);
|
|
272
|
+
|
|
273
|
+
while (match) {
|
|
274
|
+
const start = match.index;
|
|
275
|
+
const end = start + match[0].length;
|
|
276
|
+
transformed += content.slice(cursor, start);
|
|
277
|
+
transformed += eraseRangeWithSpaces(content, start, end);
|
|
278
|
+
cursor = end;
|
|
279
|
+
match = dCallRegexp.exec(content);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
transformed += content.slice(cursor);
|
|
283
|
+
return transformed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function collectChineseLines(content, maxPrintPerFile) {
|
|
287
|
+
const lines = content.split('\n');
|
|
288
|
+
const hits = [];
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
291
|
+
const line = lines[i];
|
|
292
|
+
const match = line.match(CJK_REGEXP);
|
|
293
|
+
if (!match) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
hits.push({
|
|
298
|
+
line: i + 1,
|
|
299
|
+
sample: line.trim().slice(0, 200),
|
|
300
|
+
hitCountInLine: match.length,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (hits.length >= maxPrintPerFile) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return hits;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function walkTarget(absTargetPath, files, excludeMatchers) {
|
|
312
|
+
if (!fs.existsSync(absTargetPath)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const relativePath = normalizePath(path.relative(ROOT, absTargetPath));
|
|
317
|
+
if (relativePath && !relativePath.startsWith('..') && matchPath(relativePath, excludeMatchers)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const stat = fs.statSync(absTargetPath);
|
|
322
|
+
if (stat.isFile()) {
|
|
323
|
+
files.push(absTargetPath);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const entries = fs.readdirSync(absTargetPath);
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
walkTarget(path.join(absTargetPath, entry), files, excludeMatchers);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseArgs(cliArgs) {
|
|
334
|
+
const args = cliArgs || process.argv.slice(2);
|
|
335
|
+
const getArgValue = (name) => {
|
|
336
|
+
const index = args.indexOf(name);
|
|
337
|
+
if (index === -1 || index === args.length - 1) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return args[index + 1];
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
reportOnly: args.includes('--report'),
|
|
345
|
+
silent: args.includes('--silent'),
|
|
346
|
+
configFile: getArgValue('--config') || DEFAULT_CONFIG_FILE,
|
|
347
|
+
reportFile: getArgValue('--report-file'),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function runNoZhCnGuard(cliArgs) {
|
|
352
|
+
const options = parseArgs(cliArgs);
|
|
353
|
+
const loaded = loadConfig(options.configFile);
|
|
354
|
+
const config = loaded.config;
|
|
355
|
+
const excludeMatchers = buildMatchers(config.exclude);
|
|
356
|
+
const allowMatchers = buildMatchers(config.allowChinesePaths);
|
|
357
|
+
|
|
358
|
+
const allFiles = [];
|
|
359
|
+
for (const target of config.include) {
|
|
360
|
+
walkTarget(path.resolve(ROOT, target), allFiles, excludeMatchers);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const reports = [];
|
|
364
|
+
let scannedFiles = 0;
|
|
365
|
+
let totalHitLines = 0;
|
|
366
|
+
let skippedBinaryFiles = 0;
|
|
367
|
+
|
|
368
|
+
for (const absFilePath of allFiles) {
|
|
369
|
+
const relativePath = normalizePath(path.relative(ROOT, absFilePath));
|
|
370
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!shouldScanFile(relativePath, config, excludeMatchers, allowMatchers)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const rawBuffer = fs.readFileSync(absFilePath);
|
|
378
|
+
const decoded = decodeTextBuffer(rawBuffer);
|
|
379
|
+
if (config.skipBinaryFiles && decoded.isBinary) {
|
|
380
|
+
skippedBinaryFiles += 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const scanSource = config.ignoreI18nDefault
|
|
385
|
+
? blankI18nDefaultText(decoded.text)
|
|
386
|
+
: decoded.text;
|
|
387
|
+
const hitLines = collectChineseLines(scanSource, config.maxPrintPerFile);
|
|
388
|
+
|
|
389
|
+
scannedFiles += 1;
|
|
390
|
+
if (hitLines.length === 0) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const fullHitLineCount = (scanSource.match(/[^\n]*[\u3400-\u9FFF][^\n]*/g) || []).length;
|
|
395
|
+
totalHitLines += fullHitLineCount;
|
|
396
|
+
|
|
397
|
+
reports.push({
|
|
398
|
+
file: relativePath,
|
|
399
|
+
hitLineCount: fullHitLineCount,
|
|
400
|
+
examples: hitLines,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
reports.sort((a, b) => b.hitLineCount - a.hitLineCount);
|
|
405
|
+
|
|
406
|
+
const result = {
|
|
407
|
+
generatedAt: new Date().toISOString(),
|
|
408
|
+
scannedFiles,
|
|
409
|
+
problemFiles: reports.length,
|
|
410
|
+
totalHitLines,
|
|
411
|
+
skippedBinaryFiles,
|
|
412
|
+
ignoreI18nDefault: Boolean(config.ignoreI18nDefault),
|
|
413
|
+
reports,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const reportPath = path.resolve(ROOT, options.reportFile || config.reportFile);
|
|
417
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
418
|
+
|
|
419
|
+
if (!options.silent) {
|
|
420
|
+
console.log(`[guard] config: ${normalizePath(path.relative(ROOT, loaded.configPath))}`);
|
|
421
|
+
console.log(`[guard] scanned files: ${scannedFiles}`);
|
|
422
|
+
console.log(`[guard] files with chinese: ${reports.length}`);
|
|
423
|
+
console.log(`[guard] chinese hit lines: ${totalHitLines}`);
|
|
424
|
+
if (config.skipBinaryFiles) {
|
|
425
|
+
console.log(`[guard] skipped binary files: ${skippedBinaryFiles}`);
|
|
426
|
+
}
|
|
427
|
+
console.log(`[guard] report: ${normalizePath(path.relative(ROOT, reportPath))}`);
|
|
428
|
+
|
|
429
|
+
if (reports.length > 0) {
|
|
430
|
+
console.log('\n[guard] top files:');
|
|
431
|
+
for (const item of reports.slice(0, 20)) {
|
|
432
|
+
console.log(` - ${item.file} (${item.hitLineCount} line hits)`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (options.reportOnly) {
|
|
438
|
+
process.exit(0);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
process.exit(reports.length > 0 ? 1 : 0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = runNoZhCnGuard;
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - jsx : JSX text content (outside ignored components)
|
|
9
9
|
* - jsx-svg : JSX text content INSIDE an ignored component (e.g. <svg>)
|
|
10
10
|
* NOT auto-fixable via intl.get() — needs manual SVG handling
|
|
11
|
+
* - jsx-svg-metadata : JSX text content inside <svg><title|desc|metadata>
|
|
12
|
+
* auto-fixable by nozhcn fix metadata strategy
|
|
13
|
+
* - console : console.* call text containing Chinese (can be auto-removed)
|
|
11
14
|
* - identifier : identifier names (rare, manual fix needed)
|
|
12
15
|
*
|
|
13
16
|
* Returns an array of Finding objects:
|
|
@@ -152,6 +155,17 @@ function collectCommentTokens(ast) {
|
|
|
152
155
|
return comments;
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
function isConsoleCall(nodePath) {
|
|
159
|
+
const callee = nodePath.node && nodePath.node.callee;
|
|
160
|
+
if (!callee || callee.type !== 'MemberExpression') return false;
|
|
161
|
+
if (callee.computed) return false;
|
|
162
|
+
return callee.object && callee.object.type === 'Identifier' && callee.object.name === 'console';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isInsideConsoleCall(nodePath) {
|
|
166
|
+
return !!nodePath.findParent((p) => p.isCallExpression() && isConsoleCall(p));
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
/**
|
|
156
170
|
* Main export: scan a single file and return all Chinese findings.
|
|
157
171
|
*
|
|
@@ -265,16 +279,65 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
265
279
|
checkTypes.includes('template') ||
|
|
266
280
|
checkTypes.includes('jsx') ||
|
|
267
281
|
checkTypes.includes('jsx-svg') ||
|
|
282
|
+
checkTypes.includes('jsx-svg-metadata') ||
|
|
283
|
+
checkTypes.includes('console') ||
|
|
268
284
|
checkTypes.includes('identifier');
|
|
269
285
|
|
|
270
286
|
if (needsAstScan) {
|
|
271
287
|
traverse(ast, {
|
|
272
288
|
// ── String literals ─────────────────────────────────────────────
|
|
273
289
|
StringLiteral(nodePath) {
|
|
274
|
-
if (!checkTypes.includes('string')) return;
|
|
275
290
|
const node = nodePath.node;
|
|
276
291
|
if (!primaryRegx.test(node.value)) return;
|
|
277
292
|
if (isIgnoredLine(node.loc && node.loc.start.line)) return;
|
|
293
|
+
if (checkTypes.includes('console') && isInsideConsoleCall(nodePath)) return;
|
|
294
|
+
|
|
295
|
+
// ── JSX SVG attribute detection ──────────────────────────────
|
|
296
|
+
// If this string is a JSX attribute value inside an SVG element
|
|
297
|
+
// (or other ignored component), tag it as 'jsx-svg-attr' so it
|
|
298
|
+
// can be auto-fixed via the svg attr strategy rather than being
|
|
299
|
+
// treated as a regular string literal requiring internationalisation.
|
|
300
|
+
if (nodePath.parent && nodePath.parent.type === 'JSXAttribute') {
|
|
301
|
+
let insideIgnored = false;
|
|
302
|
+
nodePath.findParent((p) => {
|
|
303
|
+
if (!p.isJSXElement()) return false;
|
|
304
|
+
const openingName = p.node.openingElement && p.node.openingElement.name;
|
|
305
|
+
if (!openingName) return false;
|
|
306
|
+
const componentName =
|
|
307
|
+
openingName.name ||
|
|
308
|
+
(openingName.namespace && openingName.namespace.name) ||
|
|
309
|
+
'';
|
|
310
|
+
if (ignoreComponents.includes(componentName)) {
|
|
311
|
+
insideIgnored = true;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (insideIgnored) {
|
|
318
|
+
if (checkTypes.includes('string') || checkTypes.includes('jsx-svg-attr')) {
|
|
319
|
+
const attrName =
|
|
320
|
+
nodePath.parent.name && nodePath.parent.name.name
|
|
321
|
+
? nodePath.parent.name.name
|
|
322
|
+
: '';
|
|
323
|
+
findings.push({
|
|
324
|
+
filePath,
|
|
325
|
+
type: 'jsx-svg-attr',
|
|
326
|
+
line: node.loc && node.loc.start.line,
|
|
327
|
+
col: node.loc && node.loc.start.column,
|
|
328
|
+
content: node.value,
|
|
329
|
+
attrName,
|
|
330
|
+
// Byte offsets (includes quote chars) — used by fixJsxSvgAttrInFile
|
|
331
|
+
start: node.start,
|
|
332
|
+
end: node.end,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return; // do NOT also report as 'string'
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Normal string literal ─────────────────────────────────────
|
|
340
|
+
if (!checkTypes.includes('string')) return;
|
|
278
341
|
if (
|
|
279
342
|
ignoreI18nDefault &&
|
|
280
343
|
isI18nDefaultValue(nodePath, i18nMethod, i18nDefaultFunctionKey, i18nObject)
|
|
@@ -300,6 +363,7 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
300
363
|
) {
|
|
301
364
|
return;
|
|
302
365
|
}
|
|
366
|
+
if (checkTypes.includes('console') && isInsideConsoleCall(nodePath)) return;
|
|
303
367
|
nodePath.node.quasis.forEach((quasi) => {
|
|
304
368
|
if (!primaryRegx.test(quasi.value.raw)) return;
|
|
305
369
|
if (isIgnoredLine(quasi.loc && quasi.loc.start.line)) return;
|
|
@@ -313,9 +377,37 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
313
377
|
});
|
|
314
378
|
},
|
|
315
379
|
|
|
380
|
+
CallExpression(nodePath) {
|
|
381
|
+
if (!checkTypes.includes('console')) return;
|
|
382
|
+
if (!isConsoleCall(nodePath)) return;
|
|
383
|
+
const node = nodePath.node;
|
|
384
|
+
const line = node.loc && node.loc.start.line;
|
|
385
|
+
if (isIgnoredLine(line)) return;
|
|
386
|
+
const rawCall = source.slice(node.start, node.end);
|
|
387
|
+
if (!primaryRegx.test(rawCall)) return;
|
|
388
|
+
|
|
389
|
+
const stmt = nodePath.parentPath && nodePath.parentPath.isExpressionStatement()
|
|
390
|
+
? nodePath.parentPath.node
|
|
391
|
+
: null;
|
|
392
|
+
|
|
393
|
+
findings.push({
|
|
394
|
+
filePath,
|
|
395
|
+
type: 'console',
|
|
396
|
+
line: (stmt && stmt.loc && stmt.loc.start.line) || line,
|
|
397
|
+
col: (stmt && stmt.loc && stmt.loc.start.column) || (node.loc && node.loc.start.column),
|
|
398
|
+
content: rawCall,
|
|
399
|
+
stmtStart: stmt && typeof stmt.start === 'number' ? stmt.start : null,
|
|
400
|
+
stmtEnd: stmt && typeof stmt.end === 'number' ? stmt.end : null,
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
|
|
316
404
|
// ── JSX text ────────────────────────────────────────────────────
|
|
317
405
|
JSXText(nodePath) {
|
|
318
|
-
|
|
406
|
+
const wantsAnyJsx =
|
|
407
|
+
checkTypes.includes('jsx') ||
|
|
408
|
+
checkTypes.includes('jsx-svg') ||
|
|
409
|
+
checkTypes.includes('jsx-svg-metadata');
|
|
410
|
+
if (!wantsAnyJsx) return;
|
|
319
411
|
const node = nodePath.node;
|
|
320
412
|
const text = node.value.trim();
|
|
321
413
|
if (!text || !primaryRegx.test(text)) return;
|
|
@@ -325,6 +417,8 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
325
417
|
// component (e.g. <svg>, <style>). If so, tag as 'jsx-svg' so
|
|
326
418
|
// the caller knows this cannot be fixed via intl.get().
|
|
327
419
|
let insideIgnored = false;
|
|
420
|
+
let insideSvgMetadata = false;
|
|
421
|
+
let metadataElement = null;
|
|
328
422
|
nodePath.findParent((p) => {
|
|
329
423
|
if (!p.isJSXElement()) return false;
|
|
330
424
|
const openingName = p.node.openingElement && p.node.openingElement.name;
|
|
@@ -334,6 +428,12 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
334
428
|
openingName.name ||
|
|
335
429
|
(openingName.namespace && openingName.namespace.name) ||
|
|
336
430
|
'';
|
|
431
|
+
|
|
432
|
+
if (!insideSvgMetadata && ['title', 'desc', 'metadata'].includes(componentName)) {
|
|
433
|
+
insideSvgMetadata = true;
|
|
434
|
+
metadataElement = p.node;
|
|
435
|
+
}
|
|
436
|
+
|
|
337
437
|
if (ignoreComponents.includes(componentName)) {
|
|
338
438
|
insideIgnored = true;
|
|
339
439
|
return true; // stop traversal
|
|
@@ -341,16 +441,37 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
341
441
|
return false;
|
|
342
442
|
});
|
|
343
443
|
|
|
344
|
-
|
|
345
|
-
if (
|
|
444
|
+
let type = 'jsx';
|
|
445
|
+
if (insideIgnored && insideSvgMetadata) {
|
|
446
|
+
type = 'jsx-svg-metadata';
|
|
447
|
+
if (!(checkTypes.includes('jsx') || checkTypes.includes('jsx-svg') || checkTypes.includes('jsx-svg-metadata'))) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
} else if (insideIgnored) {
|
|
451
|
+
type = 'jsx-svg';
|
|
452
|
+
if (!checkTypes.includes('jsx-svg')) return;
|
|
453
|
+
} else {
|
|
454
|
+
if (!checkTypes.includes('jsx')) return;
|
|
455
|
+
}
|
|
346
456
|
|
|
347
|
-
|
|
457
|
+
const finding = {
|
|
348
458
|
filePath,
|
|
349
459
|
type,
|
|
350
460
|
line: node.loc && node.loc.start.line,
|
|
351
461
|
col: node.loc && node.loc.start.column,
|
|
352
462
|
content: text,
|
|
353
|
-
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (type === 'jsx-svg-metadata') {
|
|
466
|
+
finding.start = node.start;
|
|
467
|
+
finding.end = node.end;
|
|
468
|
+
if (metadataElement) {
|
|
469
|
+
finding.metaElementStart = metadataElement.start;
|
|
470
|
+
finding.metaElementEnd = metadataElement.end;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
findings.push(finding);
|
|
354
475
|
},
|
|
355
476
|
|
|
356
477
|
// ── Identifiers (variable / function / class names) ─────────────
|
|
@@ -35,6 +35,73 @@ const log = require('./log');
|
|
|
35
35
|
// Chinese character detection regex
|
|
36
36
|
const ZH_REGEX = /[\u4e00-\u9fa5]/;
|
|
37
37
|
|
|
38
|
+
function escapeRegExp(text) {
|
|
39
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildRawStringLiteral(value, quote) {
|
|
43
|
+
const q = quote === '"' ? '"' : '\'';
|
|
44
|
+
const escaped = String(value)
|
|
45
|
+
.replace(/\\/g, '\\\\')
|
|
46
|
+
.replace(/\r/g, '\\r')
|
|
47
|
+
.replace(/\n/g, '\\n')
|
|
48
|
+
.replace(/\u2028/g, '\\u2028')
|
|
49
|
+
.replace(/\u2029/g, '\\u2029')
|
|
50
|
+
.replace(new RegExp(escapeRegExp(q), 'g'), `\\${q}`);
|
|
51
|
+
|
|
52
|
+
return `${q}${escaped}${q}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasOwnTranslation(localeMap, key) {
|
|
56
|
+
return Object.prototype.hasOwnProperty.call(localeMap, key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function replaceEmbeddedI18nDefaultInText(text, localeMap, opts, missing) {
|
|
60
|
+
const {
|
|
61
|
+
i18nObject = 'intl',
|
|
62
|
+
i18nMethod = 'get',
|
|
63
|
+
i18nDefaultFunctionKey = 'd',
|
|
64
|
+
} = opts || {};
|
|
65
|
+
|
|
66
|
+
const objectPrefix = i18nObject ? `${escapeRegExp(i18nObject)}\\s*\\.\\s*` : '';
|
|
67
|
+
const method = escapeRegExp(i18nMethod);
|
|
68
|
+
const dMethod = escapeRegExp(i18nDefaultFunctionKey);
|
|
69
|
+
|
|
70
|
+
const pattern = new RegExp(
|
|
71
|
+
`${objectPrefix}${method}\\s*\\(\\s*(["'])((?:\\\\.|(?!\\1).)*)\\1\\s*\\)\\s*\\.\\s*${dMethod}\\s*\\(\\s*(["'])((?:\\\\.|(?!\\3).)*)\\3\\s*\\)`,
|
|
72
|
+
'g'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
let replaced = 0;
|
|
76
|
+
const next = text.replace(pattern, (full, keyQuote, rawKey, valueQuote, rawValue) => {
|
|
77
|
+
const key = rawKey.replace(/\\(["'])/g, '$1');
|
|
78
|
+
const chinese = rawValue.replace(/\\(["'])/g, '$1');
|
|
79
|
+
|
|
80
|
+
if (!ZH_REGEX.test(chinese)) {
|
|
81
|
+
return full;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!hasOwnTranslation(localeMap, key)) {
|
|
85
|
+
missing.push({ key, chinese });
|
|
86
|
+
return full;
|
|
87
|
+
}
|
|
88
|
+
const translation = localeMap[key];
|
|
89
|
+
|
|
90
|
+
const escapedTranslation = String(translation)
|
|
91
|
+
.replace(/\\/g, '\\\\')
|
|
92
|
+
.replace(new RegExp(valueQuote, 'g'), `\\${valueQuote}`);
|
|
93
|
+
|
|
94
|
+
replaced += 1;
|
|
95
|
+
|
|
96
|
+
return full.replace(
|
|
97
|
+
new RegExp(`${escapeRegExp(valueQuote)}((?:\\\\.|(?!${escapeRegExp(valueQuote)}).)*)${escapeRegExp(valueQuote)}\\s*\\)$`),
|
|
98
|
+
`${valueQuote}${escapedTranslation}${valueQuote})`
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return { text: next, replaced };
|
|
103
|
+
}
|
|
104
|
+
|
|
38
105
|
/**
|
|
39
106
|
* Extract the i18n key from the "inner" `.get('key')` call that wraps a `.d()`.
|
|
40
107
|
*
|
|
@@ -145,12 +212,11 @@ module.exports = function fixI18nDefaultInFile(filePath, localeMap, opts) {
|
|
|
145
212
|
const key = extractI18nKey(node, i18nMethod, i18nDefaultFunctionKey, i18nObject);
|
|
146
213
|
if (!key) return;
|
|
147
214
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!translation) {
|
|
215
|
+
if (!hasOwnTranslation(localeMap, key)) {
|
|
151
216
|
missing.push({ key, chinese: arg.value });
|
|
152
217
|
return;
|
|
153
218
|
}
|
|
219
|
+
const translation = localeMap[key];
|
|
154
220
|
|
|
155
221
|
// Replace the argument node with a new StringLiteral containing
|
|
156
222
|
// the translation. We use Object.assign to preserve recast's
|
|
@@ -160,6 +226,38 @@ module.exports = function fixI18nDefaultInFile(filePath, localeMap, opts) {
|
|
|
160
226
|
Object.assign(arg, t.stringLiteral(translation), { extra: undefined });
|
|
161
227
|
fixed++;
|
|
162
228
|
},
|
|
229
|
+
StringLiteral(nodePath) {
|
|
230
|
+
const node = nodePath.node;
|
|
231
|
+
if (!node || typeof node.value !== 'string') return;
|
|
232
|
+
if (!ZH_REGEX.test(node.value)) return;
|
|
233
|
+
|
|
234
|
+
const result = replaceEmbeddedI18nDefaultInText(node.value, localeMap, opts, missing);
|
|
235
|
+
if (result.replaced > 0 && result.text !== node.value) {
|
|
236
|
+
const raw = node.extra && typeof node.extra.raw === 'string' ? node.extra.raw : '';
|
|
237
|
+
const originalQuote = raw.startsWith('"') ? '"' : '\'';
|
|
238
|
+
node.value = result.text;
|
|
239
|
+
node.extra = {
|
|
240
|
+
...(node.extra || {}),
|
|
241
|
+
rawValue: result.text,
|
|
242
|
+
raw: buildRawStringLiteral(result.text, originalQuote),
|
|
243
|
+
};
|
|
244
|
+
fixed += result.replaced;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
TemplateElement(nodePath) {
|
|
248
|
+
const node = nodePath.node;
|
|
249
|
+
if (!node || !node.value || typeof node.value.cooked !== 'string') return;
|
|
250
|
+
if (!ZH_REGEX.test(node.value.cooked)) return;
|
|
251
|
+
|
|
252
|
+
const result = replaceEmbeddedI18nDefaultInText(node.value.cooked, localeMap, opts, missing);
|
|
253
|
+
if (result.replaced > 0 && result.text !== node.value.cooked) {
|
|
254
|
+
Object.assign(node, t.templateElement({
|
|
255
|
+
raw: result.text,
|
|
256
|
+
cooked: result.text,
|
|
257
|
+
}, node.tail), { extra: undefined });
|
|
258
|
+
fixed += result.replaced;
|
|
259
|
+
}
|
|
260
|
+
},
|
|
163
261
|
});
|
|
164
262
|
|
|
165
263
|
if (fixed === 0) {
|
|
@@ -167,7 +265,7 @@ module.exports = function fixI18nDefaultInFile(filePath, localeMap, opts) {
|
|
|
167
265
|
}
|
|
168
266
|
|
|
169
267
|
// ── Write back using recast (minimal diff) ───────────────────────────
|
|
170
|
-
const newCode = recast.print(recastAst).code;
|
|
268
|
+
const newCode = recast.print(recastAst, { quote: 'auto' }).code;
|
|
171
269
|
try {
|
|
172
270
|
fs.writeFileSync(filePath, newCode, { encoding: 'utf-8' });
|
|
173
271
|
} catch (err) {
|
|
@@ -215,3 +215,293 @@ module.exports = function fixZhCnInFile(filePath, findings, strategy = 'clean')
|
|
|
215
215
|
|
|
216
216
|
return patchedCount;
|
|
217
217
|
};
|
|
218
|
+
|
|
219
|
+
// ─── JSX SVG attribute fix ───────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
// Strip Chinese + CJK punctuation / fullwidth forms (design-tool residue)
|
|
222
|
+
const ZH_CLEAN_REGEX = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Fix JSX SVG attribute findings (type 'jsx-svg-attr') by stripping Chinese
|
|
226
|
+
* characters from the string literal values.
|
|
227
|
+
*
|
|
228
|
+
* For each finding:
|
|
229
|
+
* - strategy 'clean' : strip Chinese (+ CJK punctuation) from the value.
|
|
230
|
+
* If the cleaned value is empty, remove the entire
|
|
231
|
+
* JSX attribute (name + '=' + value).
|
|
232
|
+
* - strategy 'remove': remove the entire JSX attribute unconditionally.
|
|
233
|
+
*
|
|
234
|
+
* Byte offsets on the finding (start / end) point to the StringLiteral node
|
|
235
|
+
* including its quote characters, as produced by findZhCnInFile.js.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} filePath - absolute file path
|
|
238
|
+
* @param {Finding[]} findings - array of { type:'jsx-svg-attr', start, end, ... }
|
|
239
|
+
* @param {'clean'|'remove'} strategy
|
|
240
|
+
* @returns {number} count of attributes fixed
|
|
241
|
+
*/
|
|
242
|
+
function fixJsxSvgAttrInFile(filePath, findings, strategy = 'clean') {
|
|
243
|
+
const attrFindings = findings.filter(
|
|
244
|
+
(f) => f.type === 'jsx-svg-attr' && typeof f.start === 'number' && typeof f.end === 'number'
|
|
245
|
+
);
|
|
246
|
+
if (!attrFindings.length) return 0;
|
|
247
|
+
|
|
248
|
+
// Defensive dedupe: duplicated ranges can cause second-pass patches to use
|
|
249
|
+
// stale offsets and accidentally trim following attributes.
|
|
250
|
+
const dedupMap = new Map();
|
|
251
|
+
attrFindings.forEach((f) => {
|
|
252
|
+
const key = `${f.start}:${f.end}`;
|
|
253
|
+
if (!dedupMap.has(key)) dedupMap.set(key, f);
|
|
254
|
+
});
|
|
255
|
+
const uniqueFindings = Array.from(dedupMap.values());
|
|
256
|
+
|
|
257
|
+
let source;
|
|
258
|
+
try {
|
|
259
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Sort descending by start so later patches don't invalidate earlier offsets
|
|
266
|
+
const sorted = [...uniqueFindings].sort((a, b) => b.start - a.start);
|
|
267
|
+
|
|
268
|
+
let patchedCount = 0;
|
|
269
|
+
|
|
270
|
+
for (const finding of sorted) {
|
|
271
|
+
const { start, end } = finding;
|
|
272
|
+
const original = source.slice(start, end); // e.g. "编组1" (with quotes)
|
|
273
|
+
if (!original || original.length < 2) continue;
|
|
274
|
+
|
|
275
|
+
const quote = original[0]; // '"' or "'"
|
|
276
|
+
// If boundaries are no longer a string literal, skip safely.
|
|
277
|
+
if ((quote !== '"' && quote !== "'") || original[original.length - 1] !== quote) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const rawValue = original.slice(1, -1);
|
|
281
|
+
const cleaned = strategy === 'remove' ? '' : rawValue.replace(ZH_CLEAN_REGEX, '').trim();
|
|
282
|
+
|
|
283
|
+
if (cleaned) {
|
|
284
|
+
// Just replace the string value in-place
|
|
285
|
+
source = source.slice(0, start) + quote + cleaned + quote + source.slice(end);
|
|
286
|
+
} else {
|
|
287
|
+
// Cleaned value is empty — remove the whole JSX attr: `attrName="value"`
|
|
288
|
+
// Walk backwards from start to find the beginning of the attribute name.
|
|
289
|
+
// Source just before start looks like: `... attrName="`
|
|
290
|
+
let attrEnd = end;
|
|
291
|
+
let pos = start - 1; // skip the opening quote's predecessor: should be '='
|
|
292
|
+
if (pos >= 0 && source[pos] === '=') pos--;
|
|
293
|
+
// Walk backwards past the attribute name characters (word chars, '-', ':')
|
|
294
|
+
while (pos >= 0 && /[\w:_-]/.test(source[pos])) pos--;
|
|
295
|
+
// pos now points to the char before the attr name (usually a space/newline)
|
|
296
|
+
const attrStart = pos + 1;
|
|
297
|
+
|
|
298
|
+
// If the attribute is on its own line (only whitespace before it on that
|
|
299
|
+
// line), remove the entire line to avoid leaving a blank line.
|
|
300
|
+
let lineStart = pos;
|
|
301
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
|
|
302
|
+
const beforeOnLine = source.slice(lineStart, attrStart);
|
|
303
|
+
const isAloneOnLine = /^\s*$/.test(beforeOnLine);
|
|
304
|
+
// Also check nothing meaningful follows on the same line
|
|
305
|
+
let afterEnd = attrEnd;
|
|
306
|
+
while (afterEnd < source.length && source[afterEnd] !== '\n' && source[afterEnd] !== '\r') afterEnd++;
|
|
307
|
+
const afterOnLine = source.slice(attrEnd, afterEnd);
|
|
308
|
+
const nothingAfter = /^\s*$/.test(afterOnLine);
|
|
309
|
+
|
|
310
|
+
if (isAloneOnLine && nothingAfter) {
|
|
311
|
+
// Remove from the start of this line (after previous \n) through the trailing \n
|
|
312
|
+
const lineEnd = afterEnd < source.length ? afterEnd + 1 : afterEnd;
|
|
313
|
+
source = source.slice(0, lineStart) + source.slice(lineEnd);
|
|
314
|
+
} else {
|
|
315
|
+
// Attribute is inline — just remove attr + one leading whitespace
|
|
316
|
+
const removeFrom = pos >= 0 && /[\s]/.test(source[pos]) ? pos : attrStart;
|
|
317
|
+
source = source.slice(0, removeFrom) + source.slice(attrEnd);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
patchedCount++;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (patchedCount === 0) return 0;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
328
|
+
} catch (err) {
|
|
329
|
+
log.error(`[nozhcn] Cannot write fixed file: ${filePath} — ${err.message}`);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return patchedCount;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Fix JSX SVG metadata text findings (type 'jsx-svg-metadata').
|
|
338
|
+
*
|
|
339
|
+
* Strategy:
|
|
340
|
+
* - 'empty' : clear only metadata text content (<title>中文</title> -> <title></title>)
|
|
341
|
+
* - 'remove' : remove the whole metadata element (<title>中文</title> -> '')
|
|
342
|
+
*
|
|
343
|
+
* @param {string} filePath
|
|
344
|
+
* @param {Finding[]} findings
|
|
345
|
+
* @param {'remove'|'empty'} strategy
|
|
346
|
+
* @returns {number}
|
|
347
|
+
*/
|
|
348
|
+
function fixJsxSvgMetadataInFile(filePath, findings, strategy = 'remove') {
|
|
349
|
+
if (!strategy) return 0;
|
|
350
|
+
if (!['remove', 'empty'].includes(strategy)) {
|
|
351
|
+
log.warnToLog(`[nozhcn] Invalid JSX SVG metadata strategy: ${strategy}, skip auto-fix.`);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const metadataFindings = findings.filter(
|
|
356
|
+
(f) => f.type === 'jsx-svg-metadata' && typeof f.start === 'number' && typeof f.end === 'number'
|
|
357
|
+
);
|
|
358
|
+
if (!metadataFindings.length) return 0;
|
|
359
|
+
|
|
360
|
+
let source;
|
|
361
|
+
try {
|
|
362
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
363
|
+
} catch (err) {
|
|
364
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let patchedCount = 0;
|
|
369
|
+
|
|
370
|
+
if (strategy === 'empty') {
|
|
371
|
+
// Clear text content only, keep metadata tags.
|
|
372
|
+
const sorted = [...metadataFindings].sort((a, b) => b.start - a.start);
|
|
373
|
+
sorted.forEach((f) => {
|
|
374
|
+
source = source.slice(0, f.start) + source.slice(f.end);
|
|
375
|
+
patchedCount++;
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
// 'remove': remove whole metadata elements; dedupe by element range.
|
|
379
|
+
const ranges = [];
|
|
380
|
+
const seen = new Set();
|
|
381
|
+
metadataFindings.forEach((f) => {
|
|
382
|
+
if (typeof f.metaElementStart !== 'number' || typeof f.metaElementEnd !== 'number') return;
|
|
383
|
+
const key = `${f.metaElementStart}:${f.metaElementEnd}`;
|
|
384
|
+
if (seen.has(key)) return;
|
|
385
|
+
seen.add(key);
|
|
386
|
+
ranges.push({ start: f.metaElementStart, end: f.metaElementEnd });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (!ranges.length) return 0;
|
|
390
|
+
|
|
391
|
+
ranges.sort((a, b) => b.start - a.start);
|
|
392
|
+
ranges.forEach(({ start, end }) => {
|
|
393
|
+
// If the metadata element occupies a whole line, remove that full line.
|
|
394
|
+
let lineStart = start;
|
|
395
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
|
|
396
|
+
const before = source.slice(lineStart, start);
|
|
397
|
+
const onlyWhitespaceBefore = /^\s*$/.test(before);
|
|
398
|
+
|
|
399
|
+
let lineEnd = end;
|
|
400
|
+
while (lineEnd < source.length && source[lineEnd] !== '\n') lineEnd++;
|
|
401
|
+
const after = source.slice(end, lineEnd);
|
|
402
|
+
const onlyWhitespaceAfter = /^\s*$/.test(after);
|
|
403
|
+
|
|
404
|
+
if (onlyWhitespaceBefore && onlyWhitespaceAfter) {
|
|
405
|
+
const removeEnd = lineEnd < source.length ? lineEnd + 1 : lineEnd;
|
|
406
|
+
source = source.slice(0, lineStart) + source.slice(removeEnd);
|
|
407
|
+
} else {
|
|
408
|
+
source = source.slice(0, start) + source.slice(end);
|
|
409
|
+
}
|
|
410
|
+
patchedCount++;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (patchedCount === 0) return 0;
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
log.error(`[nozhcn] Cannot write fixed file: ${filePath} — ${err.message}`);
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return patchedCount;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Remove console statement findings (type 'console') from a source file.
|
|
428
|
+
*
|
|
429
|
+
* We only auto-fix findings that carry ExpressionStatement ranges
|
|
430
|
+
* (`stmtStart` / `stmtEnd`). This keeps removal safe and predictable.
|
|
431
|
+
*
|
|
432
|
+
* @param {string} filePath
|
|
433
|
+
* @param {Finding[]} findings
|
|
434
|
+
* @param {'remove'|false} strategy
|
|
435
|
+
* @returns {number}
|
|
436
|
+
*/
|
|
437
|
+
function fixConsoleZhCnInFile(filePath, findings, strategy = 'remove') {
|
|
438
|
+
if (!strategy) return 0;
|
|
439
|
+
if (strategy !== 'remove') {
|
|
440
|
+
log.warnToLog(`[nozhcn] Invalid console strategy: ${strategy}, skip auto-fix.`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const consoleFindings = findings.filter(
|
|
445
|
+
(f) =>
|
|
446
|
+
f.type === 'console' &&
|
|
447
|
+
typeof f.stmtStart === 'number' &&
|
|
448
|
+
typeof f.stmtEnd === 'number'
|
|
449
|
+
);
|
|
450
|
+
if (!consoleFindings.length) return 0;
|
|
451
|
+
|
|
452
|
+
let source;
|
|
453
|
+
try {
|
|
454
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
455
|
+
} catch (err) {
|
|
456
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Deduplicate statement ranges first (a single console stmt can produce multiple findings).
|
|
461
|
+
const rangeMap = new Map();
|
|
462
|
+
consoleFindings.forEach((f) => {
|
|
463
|
+
const key = `${f.stmtStart}:${f.stmtEnd}`;
|
|
464
|
+
if (!rangeMap.has(key)) {
|
|
465
|
+
rangeMap.set(key, { start: f.stmtStart, end: f.stmtEnd });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const ranges = Array.from(rangeMap.values()).sort((a, b) => b.start - a.start);
|
|
470
|
+
let patchedCount = 0;
|
|
471
|
+
|
|
472
|
+
ranges.forEach(({ start, end }) => {
|
|
473
|
+
let lineStart = start;
|
|
474
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
|
|
475
|
+
const before = source.slice(lineStart, start);
|
|
476
|
+
const onlyWhitespaceBefore = /^\s*$/.test(before);
|
|
477
|
+
|
|
478
|
+
let lineEnd = end;
|
|
479
|
+
while (lineEnd < source.length && source[lineEnd] !== '\n') lineEnd++;
|
|
480
|
+
const after = source.slice(end, lineEnd);
|
|
481
|
+
const onlyWhitespaceAfter = /^\s*$/.test(after);
|
|
482
|
+
|
|
483
|
+
if (onlyWhitespaceBefore && onlyWhitespaceAfter) {
|
|
484
|
+
const removeEnd = lineEnd < source.length ? lineEnd + 1 : lineEnd;
|
|
485
|
+
source = source.slice(0, lineStart) + source.slice(removeEnd);
|
|
486
|
+
} else {
|
|
487
|
+
source = source.slice(0, start) + source.slice(end);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
patchedCount++;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (patchedCount === 0) return 0;
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
497
|
+
} catch (err) {
|
|
498
|
+
log.error(`[nozhcn] Cannot write fixed file: ${filePath} — ${err.message}`);
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return patchedCount;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
module.exports.fixJsxSvgAttrInFile = fixJsxSvgAttrInFile;
|
|
506
|
+
module.exports.fixJsxSvgMetadataInFile = fixJsxSvgMetadataInFile;
|
|
507
|
+
module.exports.fixConsoleZhCnInFile = fixConsoleZhCnInFile;
|