jacobinwwey_obsidian-NotEMD/scripts/audit-hardcoded-ui-strings.js
2026-07-21 23:30:31 +08:00

126 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const projectRoot = process.cwd();
const candidateFiles = [
'src/main.ts',
'src/fileUtils.ts',
'src/searchUtils.ts',
'src/translate.ts',
'src/extractOriginalText.ts',
'src/providerDiagnostics.ts',
'src/ui'
];
const ignoreFilePatterns = [
/src\/i18n\//,
/src\/tests\//,
/src\/ui\/ErrorModal\.ts$/,
/src\/ui\/ProgressModal\.ts$/
];
const ignoreLinePatterns = [
/getSidebarActionLabel/,
/getI18nStrings/,
/formatI18n/,
/i18n\./,
/strings\./,
/provider\.name/,
/lang\.name/,
/issue\.message/,
/Unsupported action:/,
/console\./
];
const matchers = [
{ kind: 'notice', regex: /new Notice\((['"`])/ },
{ kind: 'setting-name', regex: /\.setName\((['"`])/ },
{ kind: 'setting-desc', regex: /\.setDesc\((['"`])/ },
{ kind: 'button-text', regex: /\.setButtonText\((['"`])/ },
{ kind: 'title-text', regex: /titleEl\.setText\((['"`])/ },
{ kind: 'element-text', regex: /createEl\([^)]*\{\s*text:\s*(['"`])/ },
{ kind: 'name-prop', regex: /\bname:\s*(['"`])(?!\1)/ },
{ kind: 'placeholder', regex: /\.setPlaceholder\((['"`])/ },
{ kind: 'status-bar', regex: /updateStatusBar\((['"`])/ },
{ kind: 'progress-status', regex: /\.updateStatus\((['"`])/ },
{ kind: 'sidebar-start', regex: /\.startProcessing\((['"`])/ }
];
function walk(entry, files = []) {
const stat = fs.statSync(entry);
if (stat.isDirectory()) {
for (const child of fs.readdirSync(entry)) {
walk(path.join(entry, child), files);
}
return files;
}
if (entry.endsWith('.ts')) {
files.push(entry);
}
return files;
}
function shouldInspect(relativePath) {
if (ignoreFilePatterns.some(pattern => pattern.test(relativePath))) {
return false;
}
return candidateFiles.some(candidate => {
const absoluteCandidate = path.join(projectRoot, candidate);
return relativePath === candidate || relativePath.startsWith(`${candidate}/`) || path.resolve(path.join(projectRoot, relativePath)) === absoluteCandidate;
});
}
function lineIsIgnored(line) {
return ignoreLinePatterns.some(pattern => pattern.test(line));
}
const allFiles = candidateFiles
.flatMap(candidate => {
const absolute = path.join(projectRoot, candidate);
return fs.existsSync(absolute) ? walk(absolute) : [];
})
.map(file => path.relative(projectRoot, file).split(path.sep).join('/'))
.filter(shouldInspect)
.sort();
const findings = [];
for (const relativeFile of allFiles) {
const absoluteFile = path.join(projectRoot, relativeFile);
const lines = fs.readFileSync(absoluteFile, 'utf8').split(/\r?\n/);
lines.forEach((line, index) => {
if (lineIsIgnored(line)) {
return;
}
for (const matcher of matchers) {
if (matcher.regex.test(line)) {
findings.push({
file: relativeFile,
line: index + 1,
kind: matcher.kind,
text: line.trim()
});
break;
}
}
});
}
if (findings.length === 0) {
console.log('No hardcoded UI string candidates found.');
process.exit(0);
}
console.log(`Found ${findings.length} hardcoded UI string candidate(s):\n`);
for (const finding of findings) {
console.log(`${finding.file}:${finding.line} [${finding.kind}] ${finding.text}`);
}
process.exit(1);