97 lines
2.8 KiB
JavaScript
97 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const {
|
|
PROJECT_ROOT,
|
|
DEFAULT_ALLOWED_EXTENSIONS,
|
|
collectSourceFiles,
|
|
toProjectRelativePath,
|
|
} = require('./source-files.cjs');
|
|
|
|
const SOURCE_DIRECTORIES = [
|
|
path.join(PROJECT_ROOT, 'backend', 'src'),
|
|
path.join(PROJECT_ROOT, 'src'),
|
|
path.join(PROJECT_ROOT, 'scripts'),
|
|
];
|
|
const IGNORED_RELATIVE_PATHS = new Set([
|
|
'scripts/check-source-quality.cjs',
|
|
'scripts/fix-source-text.cjs',
|
|
]);
|
|
const ALLOWED_EXTENSIONS = DEFAULT_ALLOWED_EXTENSIONS;
|
|
|
|
const MOJIBAKE_PATTERNS = [
|
|
{ label: 'UTF8_AS_LATIN1', regex: /\u00C3[\u0080-\u00BF]/u },
|
|
{ label: 'REPLACEMENT_CHAR', regex: /\uFFFD/u },
|
|
{ label: 'MISDECODED_QUESTION_MARK', regex: /\u00EF\u00BF\u00BD/u },
|
|
{ label: 'CP1252_ARTIFACT', regex: /\u00C2[\u0080-\u00BF]?/u },
|
|
{
|
|
label: 'DOUBLE_DECODED_APOSTROPHE',
|
|
regex: /\u00E2\u20AC\u2122|\u00E2\u20AC\u0153|\u00E2\u20AC\u009D/u,
|
|
},
|
|
];
|
|
|
|
const ASCII_TRANSLITERATION_PATTERNS = [
|
|
{
|
|
label: 'ASCII_TRANSLITERATION',
|
|
regex:
|
|
/\b(?:fuer|Fuer|gueltig|Gueltig|ungueltig|Ungueltig|zurueck|Zurueck|ueberein|Ueberein|waehlen|Waehlen|zaehler|Zaehler|ausfuellen|Ausfuellen|geschuetzt|Geschuetzt|verschluesselt|Verschluesselt)\b/u,
|
|
},
|
|
];
|
|
|
|
const VAR_PATTERN = /\bvar\s+[A-Za-z_$][A-Za-z0-9_$]*/u;
|
|
|
|
/** @type {Array<{file: string, rule: string, detail: string}>} */
|
|
const issues = [];
|
|
|
|
for (const directoryPath of SOURCE_DIRECTORIES) {
|
|
const files = collectSourceFiles(directoryPath, ALLOWED_EXTENSIONS);
|
|
for (const filePath of files) {
|
|
const source = fs.readFileSync(filePath, 'utf8');
|
|
const relativePath = toProjectRelativePath(filePath);
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
if (IGNORED_RELATIVE_PATHS.has(relativePath)) {
|
|
continue;
|
|
}
|
|
|
|
for (const pattern of MOJIBAKE_PATTERNS) {
|
|
if (pattern.regex.test(source)) {
|
|
issues.push({
|
|
file: relativePath,
|
|
rule: pattern.label,
|
|
detail: 'possible mojibake detected',
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const pattern of ASCII_TRANSLITERATION_PATTERNS) {
|
|
if (pattern.regex.test(source)) {
|
|
issues.push({
|
|
file: relativePath,
|
|
rule: pattern.label,
|
|
detail: 'possible ASCII transliteration detected',
|
|
});
|
|
}
|
|
}
|
|
|
|
if (extension !== '.html' && VAR_PATTERN.test(source)) {
|
|
issues.push({
|
|
file: relativePath,
|
|
rule: 'NO_VAR',
|
|
detail: 'use const/let instead of var',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (issues.length === 0) {
|
|
console.log('Source quality check passed: no mojibake artifacts and no var declarations found.');
|
|
process.exit(0);
|
|
}
|
|
|
|
console.error('Source quality check failed:');
|
|
for (const issue of issues) {
|
|
console.error(`- ${issue.file} [${issue.rule}] ${issue.detail}`);
|
|
}
|
|
process.exit(1);
|