Compare commits
2 Commits
1f293c3d03
...
3331e6d66e
| Author | SHA1 | Date |
|---|---|---|
|
|
3331e6d66e | |
|
|
2f3eae757c |
|
|
@ -173,27 +173,30 @@ function pushStatementIntoSchemaAst(moduleName, statement, sourceFile) {
|
|||
*/
|
||||
function checkActionDefNameConsistent(filename, actionDefNode) {
|
||||
const { name, type } = actionDefNode;
|
||||
(0, assert_1.default)(ts.isTypeReferenceNode(type), "ActionDef应该是一个类型引用");
|
||||
(0, assert_1.default)(ts.isTypeReferenceNode(type), `「${filename}」中的 ActionDef 定义错误:必须是类型引用,如 ActionDef<CreateAction, CreateState>`);
|
||||
const { typeArguments } = type;
|
||||
(0, assert_1.default)(typeArguments.length === 2);
|
||||
(0, assert_1.default)(typeArguments.length === 2, `「${filename}」中的 ActionDef 定义错误:必须有两个类型参数 <Action, State>`);
|
||||
const [actionNode, stateNode] = typeArguments;
|
||||
(0, assert_1.default)(ts.isIdentifier(name), `文件${filename}中的ActionDef${name.text}不是一个有效的变量`);
|
||||
(0, assert_1.default)(name.text.endsWith('ActionDef'), `文件${filename}中的ActionDef${name.text}未以ActionDef结尾`);
|
||||
(0, assert_1.default)(ts.isTypeReferenceNode(actionNode) && ts.isTypeReferenceNode(stateNode), `文件${filename}中的ActionDef${name.text}类型声明中的action和state非法`);
|
||||
(0, assert_1.default)(ts.isIdentifier(actionNode.typeName) && ts.isIdentifier(stateNode.typeName));
|
||||
(0, assert_1.default)(actionNode.typeName.text.endsWith('Action'), `文件${filename}中的ActionDef${name.text}所引用的Action${actionNode.typeName}未以Action结尾`);
|
||||
(0, assert_1.default)(stateNode.typeName.text.endsWith('State'), `文件${filename}中的ActionDef${name.text}所引用的State${stateNode.typeName}未以State结尾`);
|
||||
(0, assert_1.default)(ts.isIdentifier(name), `「${filename}」中的 ActionDef「${name.text}」不是有效的变量名`);
|
||||
(0, assert_1.default)(name.text.endsWith('ActionDef'), `「${filename}」中的变量「${name.text}」命名错误:ActionDef 变量必须以 'ActionDef' 结尾`);
|
||||
(0, assert_1.default)(ts.isTypeReferenceNode(actionNode) && ts.isTypeReferenceNode(stateNode), `「${filename}」中的「${name.text}」类型参数错误:Action 和 State 必须是类型引用`);
|
||||
(0, assert_1.default)(ts.isIdentifier(actionNode.typeName) && ts.isIdentifier(stateNode.typeName), `「${filename}」中的「${name.text}」类型参数错误:Action 和 State 必须是简单标识符`);
|
||||
(0, assert_1.default)(actionNode.typeName.text.endsWith('Action'), `「${filename}」中的「${name.text}」引用的 Action 类型「${actionNode.typeName.text}」命名错误:必须以 'Action' 结尾`);
|
||||
(0, assert_1.default)(stateNode.typeName.text.endsWith('State'), `「${filename}」中的「${name.text}」引用的 State 类型「${stateNode.typeName.text}」命名错误:必须以 'State' 结尾`);
|
||||
const adfName = name.text.slice(0, name.text.length - 9);
|
||||
const aName = actionNode.typeName.text.slice(0, actionNode.typeName.text.length - 6);
|
||||
const sName = stateNode.typeName.text.slice(0, stateNode.typeName.text.length - 5);
|
||||
(0, assert_1.default)(adfName === aName && aName === sName, `文件${filename}中的ActionDef${name.text}中ActionDef, Action和State的命名规则不一致, 需要
|
||||
${adfName}ActionDef, ${adfName}Action, ${adfName}State`);
|
||||
(0, assert_1.default)(adfName === aName && aName === sName, `「${filename}」中的「${name.text}」命名不一致。\n` +
|
||||
`要求:ActionDef、Action、State 的前缀必须相同\n` +
|
||||
`当前:${name.text}, ${actionNode.typeName.text}, ${stateNode.typeName.text}\n` +
|
||||
`应为:${adfName}ActionDef, ${adfName}Action, ${adfName}State`);
|
||||
}
|
||||
function checkStringLiteralLegal(filename, obj, text, ele) {
|
||||
(0, assert_1.default)(ts.isLiteralTypeNode(ele) && ts.isStringLiteral(ele.literal), `${filename}中引用的${obj} ${text}中存在不是stringliteral的类型`);
|
||||
(0, assert_1.default)(!ele.literal.text.includes('$'), `${filename}中引用的action${text}中的${obj}「${ele.literal.text}」包含非法字符$`);
|
||||
(0, assert_1.default)(ele.literal.text.length > 0, `${filename}中引用的action${text}中的${obj}「${ele.literal.text}」长度非法`);
|
||||
(0, assert_1.default)(ele.literal.text.length < env_1.STRING_LITERAL_MAX_LENGTH, `${filename}中引用的${obj} ${text}中的「${ele.literal.text}」长度过长`);
|
||||
(0, assert_1.default)(ts.isLiteralTypeNode(ele) && ts.isStringLiteral(ele.literal), `「${filename}」中的 ${obj}「${text}」类型错误:必须是字符串字面量类型,如 'create' | 'update'`);
|
||||
(0, assert_1.default)(!ele.literal.text.includes('$'), `「${filename}」中的 ${obj}「${text}」包含非法字符:「${ele.literal.text}」中不能包含 '$' 字符`);
|
||||
(0, assert_1.default)(ele.literal.text.length > 0, `「${filename}」中的 ${obj}「${text}」不能为空字符串`);
|
||||
(0, assert_1.default)(ele.literal.text.length < env_1.STRING_LITERAL_MAX_LENGTH, `「${filename}」中的 ${obj}「${text}」的值「${ele.literal.text}」长度超限。\n` +
|
||||
`当前长度:${ele.literal.text.length},最大允许:${env_1.STRING_LITERAL_MAX_LENGTH}`);
|
||||
return ele.literal.text;
|
||||
}
|
||||
function addImportedFrom(moduleName, name, node) {
|
||||
|
|
@ -395,12 +398,14 @@ function dealWithActionTypeNode(moduleName, filename, actionTypeNode, program, s
|
|||
return ele.typeName.text;
|
||||
}
|
||||
}).filter(ele => !!ele);
|
||||
(0, assert_1.default)((0, lodash_1.intersection)(actionNames, RESERVED_ACTION_NAMES).length === 0, `${filename}中的Action命名不能是「${RESERVED_ACTION_NAMES.join(',')}」之一`);
|
||||
(0, assert_1.default)((0, lodash_1.intersection)(actionNames, RESERVED_ACTION_NAMES).length === 0, `「${filename}」中的 Action 命名冲突:不能使用保留名称。\n` +
|
||||
`保留名称:${RESERVED_ACTION_NAMES.join(', ')}\n` +
|
||||
`冲突的名称:${(0, lodash_1.intersection)(actionNames, RESERVED_ACTION_NAMES).join(', ')}`);
|
||||
actionTypeNode.types.forEach(ele => {
|
||||
if (ts.isTypeReferenceNode(ele)) {
|
||||
// 这里递归了类型定义,去查找所有的action字符串
|
||||
const actionStrings = tryGetStringLiteralValues(moduleName, filename, 'action', ele, program);
|
||||
(0, assert_1.default)(actionStrings.length > 0, `${filename}中的Action所引用的Type定义为空`);
|
||||
(0, assert_1.default)(actionStrings.length > 0, `「${filename}」中的 Action 类型「${ele.typeName.text}」定义为空,必须至少包含一个 action`);
|
||||
actionTexts.push(...actionStrings);
|
||||
}
|
||||
else {
|
||||
|
|
@ -411,7 +416,9 @@ function dealWithActionTypeNode(moduleName, filename, actionTypeNode, program, s
|
|||
}
|
||||
else if (ts.isTypeReferenceNode(actionTypeNode)) {
|
||||
if (ts.isIdentifier(actionTypeNode.typeName)) {
|
||||
(0, assert_1.default)(!RESERVED_ACTION_NAMES.includes(actionTypeNode.typeName.text), `${filename}中的Action命名不能是「${RESERVED_ACTION_NAMES.join(',')}」之一`);
|
||||
(0, assert_1.default)((0, lodash_1.intersection)([actionTypeNode.typeName.text], RESERVED_ACTION_NAMES).length === 0, `「${filename}」中的 Action 命名冲突:不能使用保留名称。\n` +
|
||||
`保留名称:${RESERVED_ACTION_NAMES.join(', ')}\n` +
|
||||
`冲突的名称:${(0, lodash_1.intersection)([actionTypeNode.typeName.text], RESERVED_ACTION_NAMES).join(', ')}`);
|
||||
}
|
||||
const actionStrings = tryGetStringLiteralValues(moduleName, filename, 'action', actionTypeNode, program);
|
||||
(0, assert_1.default)(actionStrings.length > 0, `${filename}中的Action定义为空`);
|
||||
|
|
@ -424,8 +431,9 @@ function dealWithActionTypeNode(moduleName, filename, actionTypeNode, program, s
|
|||
// 所有的action定义不能有重名
|
||||
const ActionDict = {};
|
||||
actionTexts.forEach((action) => {
|
||||
(0, assert_1.default)(action.length <= env_1.STRING_LITERAL_MAX_LENGTH, `${filename}中的Action「${action}」命名长度大于${env_1.STRING_LITERAL_MAX_LENGTH}`);
|
||||
(0, assert_1.default)(/^[a-z][a-z|A-Z]+$/.test(action), `${filename}中的Action「${action}」命名不合法,必须以小字字母开头且只能包含字母`);
|
||||
(0, assert_1.default)(action.length <= env_1.STRING_LITERAL_MAX_LENGTH, `「${filename}」中的 Action「${action}」命名长度超限。\n` +
|
||||
`当前长度:${action.length},最大允许:${env_1.STRING_LITERAL_MAX_LENGTH}`);
|
||||
(0, assert_1.default)(/^[a-z][a-zA-Z]+$/.test(action), `「${filename}」中的 Action「${action}」命名格式错误:必须以小写字母开头且只能包含字母`);
|
||||
if (ActionDict.hasOwnProperty(action)) {
|
||||
throw new Error(`文件${filename}中,Action定义上的【${action}】动作存在同名`);
|
||||
}
|
||||
|
|
@ -644,15 +652,16 @@ function checkLocaleExpressionPropertyExists(root, attr, exists, filename) {
|
|||
}
|
||||
} */
|
||||
function checkNameLegal(filename, attrName, upperCase) {
|
||||
(0, assert_1.default)(attrName.length <= env_1.ENTITY_NAME_MAX_LENGTH, `文件「${filename}」:「${attrName}」的名称定义过长,不能超过「${env_1.ENTITY_NAME_MAX_LENGTH}」长度`);
|
||||
(0, assert_1.default)(attrName.length <= env_1.ENTITY_NAME_MAX_LENGTH, `「${filename}」中的名称「${attrName}」长度超限。\n` +
|
||||
`当前长度:${attrName.length},最大允许:${env_1.ENTITY_NAME_MAX_LENGTH}`);
|
||||
if (upperCase) {
|
||||
(0, assert_1.default)(/[A-Z][a-z|A-Z|0-9]+/i.test(attrName), `文件「${filename}」:「${attrName}」的名称必须以大写字母开始,且只能包含字母和数字`);
|
||||
(0, assert_1.default)(/[A-Z][a-z|A-Z|0-9]+/i.test(attrName), `「${filename}」中的名称「${attrName}」格式错误:必须以大写字母开头,只能包含字母和数字(如:UserProfile)`);
|
||||
}
|
||||
else if (upperCase === false) {
|
||||
(0, assert_1.default)(/[a-z][a-z|A-Z|0-9]+/i.test(attrName), `文件「${filename}」:「${attrName}」的名称必须以小写字母开始,且只能包含字母和数字`);
|
||||
(0, assert_1.default)(/[a-z][a-z|A-Z|0-9]+/i.test(attrName), `「${filename}」中的名称「${attrName}」格式错误:必须以小写字母开头,只能包含字母和数字(如:userName)`);
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(/[a-z|A-Z][a-z|A-Z|0-9]+/i.test(attrName), `文件「${filename}」:「${attrName}」的名称必须以字母开始,且只能包含字母和数字`);
|
||||
(0, assert_1.default)(/[a-z|A-Z][a-z|A-Z|0-9]+/i.test(attrName), `「${filename}」中的名称「${attrName}」格式错误:必须以字母开头,只能包含字母和数字`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -838,7 +847,8 @@ function analyzeSchemaDefinition(node, moduleName, filename, path, program, refe
|
|||
let toLog = false;
|
||||
const extendsFrom = [];
|
||||
const { members, heritageClauses } = node;
|
||||
(0, assert_1.default)(heritageClauses, `「${filename}」中的Schema定义需要继承EntityShape或者其他Entity`);
|
||||
(0, assert_1.default)(heritageClauses, `「${filename}」中的 Schema 定义错误:必须继承 EntityShape 或其他实体。\n` +
|
||||
`示例:export interface Schema extends EntityShape { ... }`);
|
||||
const heritagedResult = heritageClauses.map((clause) => {
|
||||
const { expression } = clause.types[0];
|
||||
(0, assert_1.default)(ts.isIdentifier(expression), `${expression}不是一个合法的继承类型`);
|
||||
|
|
@ -847,7 +857,8 @@ function analyzeSchemaDefinition(node, moduleName, filename, path, program, refe
|
|||
return;
|
||||
}
|
||||
// 从其它文件的Schema类继承,这里需要将所继承的对象的属性进行访问并展开
|
||||
(0, assert_1.default)(referencedSchemas.includes(expression.text), `「${filename}」中的Schema继承了未定义的实体「${expression.text}」`);
|
||||
(0, assert_1.default)(referencedSchemas.includes(expression.text), `「${filename}」中的 Schema 继承了未导入的实体「${expression.text}」。\n` +
|
||||
`提示:请先导入该实体:import { Schema as ${expression.text} } from '...'`);
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(expression);
|
||||
let declaration = symbol?.getDeclarations()[0];
|
||||
|
|
@ -879,11 +890,19 @@ function analyzeSchemaDefinition(node, moduleName, filename, path, program, refe
|
|||
else if (type.typeName.text === 'Array') {
|
||||
// 这是一对多的反向指针的引用,需要特殊处理
|
||||
const { typeArguments } = type;
|
||||
(0, assert_1.default)(typeArguments.length === 1
|
||||
&& ts.isTypeReferenceNode(typeArguments[0])
|
||||
&& ts.isIdentifier(typeArguments[0].typeName)
|
||||
&& referencedSchemas.includes(typeArguments[0].typeName.text), `「${filename}」非法的属性定义「${attrName}」`);
|
||||
const reverseEntity = typeArguments[0].typeName.text;
|
||||
(0, assert_1.default)(typeArguments && typeArguments.length === 1, `「${filename}」的属性「${attrName}」:Array 类型必须有且仅有一个类型参数`);
|
||||
const elementType = typeArguments[0];
|
||||
(0, assert_1.default)(ts.isTypeReferenceNode(elementType), `「${filename}」的属性「${attrName}」:Array<${elementType.getText()}> 的元素类型必须是实体引用,用于记录反向指针关系。\n` +
|
||||
`提示:\n` +
|
||||
` - 如需存储字符串数组,请使用 string[] 类型或者自定义类型存储 JSON:tags?: string[]\n` +
|
||||
` - 如需关联实体,请使用实体类型:attachments?: Array<ExtraFile>`);
|
||||
(0, assert_1.default)(ts.isIdentifier(elementType.typeName), `「${filename}」的属性「${attrName}」:Array 的元素类型必须是简单标识符`);
|
||||
const elementTypeName = elementType.typeName.text;
|
||||
(0, assert_1.default)(referencedSchemas.includes(elementTypeName), `「${filename}」的属性「${attrName}」:Array<${elementTypeName}> 中的 ${elementTypeName} 不是有效的实体引用。\n` +
|
||||
`提示:\n` +
|
||||
` - 请确保已导入该实体:import { Schema as ${elementTypeName} } from '...';\n` +
|
||||
` - 或改用 Object 类型或者自定义类型存储 JSON 数据`);
|
||||
const reverseEntity = elementTypeName;
|
||||
if (ReversePointerRelations[reverseEntity]) {
|
||||
if (!ReversePointerRelations[reverseEntity].includes(moduleName)) {
|
||||
ReversePointerRelations[reverseEntity].push(moduleName);
|
||||
|
|
@ -1042,7 +1061,8 @@ function analyzeReferenceSchemaFile(moduleName, filename, path, sourceFile, prog
|
|||
}
|
||||
}
|
||||
});
|
||||
(0, assert_1.default)(result, `「${filename}」中没有找到Schema定义`);
|
||||
(0, assert_1.default)(result, `「${filename}」中缺少 Schema 定义。\n` +
|
||||
`提示:每个实体文件必须包含 'export interface Schema extends EntityShape { ... }'`);
|
||||
return result;
|
||||
}
|
||||
function analyzeEntity(filename, path, program, relativePath) {
|
||||
|
|
@ -1913,7 +1933,8 @@ function constructFilter(statements, entity) {
|
|||
}
|
||||
else {
|
||||
// 此时应当是引用本地定义的shape
|
||||
(0, assert_1.default)(type, `${entity}中的属性${name.toString()}有非法的属性类型定义`);
|
||||
(0, assert_1.default)(type, `「${entity}」的属性「${name.toString()}」缺少类型定义。\n` +
|
||||
`提示:每个属性都必须明确指定类型,如:name: String<32>`);
|
||||
members.push(factory.createPropertySignature(undefined, name, undefined, factory.createTypeReferenceNode(factory.createIdentifier('JsonFilter'), [
|
||||
type
|
||||
])));
|
||||
|
|
@ -4174,11 +4195,12 @@ function constructAttributes(entity) {
|
|||
if (ts.isUnionTypeNode(type)) {
|
||||
if (ts.isLiteralTypeNode(type.types[0])) {
|
||||
if (ts.isStringLiteral(type.types[0].literal)) {
|
||||
(0, assert_1.default)(enumAttributes && enumAttributes[name.text], `${entity}对象中的${name.text}属性没有定义enumAttributes`);
|
||||
(0, assert_1.default)(enumAttributes && enumAttributes[name.text], `「${entity}」的属性「${name.text}」缺少枚举值定义。\n` +
|
||||
`提示:枚举类型属性需要在 enumAttributes 中定义可选值`);
|
||||
attrAssignments.push(factory.createPropertyAssignment('type', factory.createStringLiteral("enum")), factory.createPropertyAssignment('enumeration', factory.createArrayLiteralExpression(enumAttributes[name.text].map(ele => factory.createStringLiteral(ele)))));
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(ts.isNumericLiteral(type.types[0].literal), `${entity}对象中的${type.types[0].literal.getText()}属性不是数字`);
|
||||
(0, assert_1.default)(ts.isNumericLiteral(type.types[0].literal), `「${entity}」的属性类型错误:「${type.types[0].literal.getText()}」不是有效的数字字面量`);
|
||||
attrAssignments.push(factory.createPropertyAssignment(factory.createIdentifier("type"), factory.createStringLiteral("int")), factory.createPropertyAssignment(factory.createIdentifier("params"), factory.createObjectLiteralExpression([
|
||||
factory.createPropertyAssignment(factory.createIdentifier("width"), factory.createNumericLiteral(env_1.INT_LITERL_DEFAULT_WIDTH))
|
||||
], true)));
|
||||
|
|
@ -4195,7 +4217,7 @@ function constructAttributes(entity) {
|
|||
attrAssignments.push(factory.createPropertyAssignment(factory.createIdentifier("type"), factory.createStringLiteral("varchar")), factory.createPropertyAssignment(factory.createIdentifier("params"), factory.createObjectLiteralExpression([factory.createPropertyAssignment(factory.createIdentifier("length"), factory.createNumericLiteral(env_1.STRING_LITERAL_MAX_LENGTH))], true)));
|
||||
}
|
||||
else {
|
||||
(0, assert_1.default)(ts.isNumericLiteral(type.literal), `${entity}对象中的${name.text}属性不是数字`);
|
||||
(0, assert_1.default)(ts.isNumericLiteral(type.literal), `「${entity}」的属性「${name.text}」类型错误:必须是数字字面量类型`);
|
||||
attrAssignments.push(factory.createPropertyAssignment(factory.createIdentifier("type"), factory.createStringLiteral("precision")), factory.createPropertyAssignment(factory.createIdentifier("params"), factory.createObjectLiteralExpression([
|
||||
factory.createPropertyAssignment(factory.createIdentifier("precision"), factory.createNumericLiteral(env_1.NUMERICAL_LITERL_DEFAULT_PRECISION)),
|
||||
factory.createPropertyAssignment(factory.createIdentifier("scale"), factory.createNumericLiteral(env_1.NUMERICAL_LITERL_DEFAULT_SCALE))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ interface OakBuildChecksConfig {
|
|||
tFunctionModules?: string[];
|
||||
checkTemplateLiterals?: boolean;
|
||||
warnStringKeys?: boolean;
|
||||
checkJsxLiterals?: boolean;
|
||||
jsxLiteralPattern?: string;
|
||||
};
|
||||
}
|
||||
interface CustomDiagnostic {
|
||||
|
|
|
|||
|
|
@ -2549,6 +2549,129 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
|
|||
});
|
||||
return diagnostics;
|
||||
};
|
||||
const checkJsxLiterals = (pwd, program, typeChecker, customConfig) => {
|
||||
const diagnostics = [];
|
||||
// 默认匹配中文字符的正则
|
||||
const defaultPattern = /[\u4e00-\u9fa5]/;
|
||||
const patternStr = customConfig.locale?.jsxLiteralPattern;
|
||||
const pattern = patternStr ? new RegExp(patternStr) : defaultPattern;
|
||||
const addDiagnostic = (node, text, messageText, code) => {
|
||||
const sourceFile = node.getSourceFile();
|
||||
diagnostics.push({
|
||||
file: sourceFile,
|
||||
start: node.getStart(sourceFile),
|
||||
length: node.getWidth(sourceFile),
|
||||
messageText,
|
||||
category: ts.DiagnosticCategory.Warning,
|
||||
code,
|
||||
reason: 'HardcodedJsxText',
|
||||
reasonDetails: [
|
||||
`✘ 发现硬编码文本: "${text}"`,
|
||||
'建议: 使用 t() 函数进行国际化处理'
|
||||
]
|
||||
});
|
||||
};
|
||||
// 检查文本是否需要国际化
|
||||
const needsI18n = (text) => {
|
||||
// 移除前后空白
|
||||
const trimmed = text.trim();
|
||||
// 忽略空字符串
|
||||
if (trimmed.length === 0)
|
||||
return false;
|
||||
// 忽略纯数字
|
||||
if (/^\d+$/.test(trimmed))
|
||||
return false;
|
||||
// 忽略纯标点符号和特殊字符
|
||||
if (/^[^\w\u4e00-\u9fa5]+$/.test(trimmed))
|
||||
return false;
|
||||
// 匹配需要国际化的模式
|
||||
return pattern.test(trimmed);
|
||||
};
|
||||
// 遍历所有源文件
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
// 跳过声明文件
|
||||
if (sourceFile.isDeclarationFile)
|
||||
continue;
|
||||
// 只检查 tsx 文件
|
||||
if (!sourceFile.fileName.endsWith('.tsx'))
|
||||
continue;
|
||||
// 检查是否在检查范围内
|
||||
if (!isFileInCheckScope(pwd, sourceFile.fileName, customConfig))
|
||||
continue;
|
||||
const visit = (node) => {
|
||||
// 检查 JsxText 节点
|
||||
if (ts.isJsxText(node)) {
|
||||
// 检查是否有忽略注释
|
||||
if (hasIgnoreComment(node, sourceFile)) {
|
||||
return;
|
||||
}
|
||||
const text = node.getText(sourceFile);
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(node, text.trim(), `JSX 文本包含需要国际化的内容: "${text.trim()}"`, 9300);
|
||||
}
|
||||
}
|
||||
// 检查 JsxAttribute 节点
|
||||
if (ts.isJsxAttribute(node)) {
|
||||
// 检查是否有忽略注释
|
||||
if (hasIgnoreComment(node, sourceFile)) {
|
||||
return;
|
||||
}
|
||||
const initializer = node.initializer;
|
||||
if (!initializer) {
|
||||
ts.forEachChild(node, visit);
|
||||
return;
|
||||
}
|
||||
// 情况1: 直接字符串字面量 desc="text"
|
||||
if (ts.isStringLiteral(initializer)) {
|
||||
const text = initializer.text;
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(initializer, text, `JSX 属性包含需要国际化的内容: ${node.name.getText()}="${text}"`, 9301);
|
||||
}
|
||||
}
|
||||
// 情况2: JSX 表达式 desc={'text'} 或 desc={`text`}
|
||||
else if (ts.isJsxExpression(initializer)) {
|
||||
const expression = initializer.expression;
|
||||
if (!expression) {
|
||||
ts.forEachChild(node, visit);
|
||||
return;
|
||||
}
|
||||
// 检查字符串字面量
|
||||
if (ts.isStringLiteral(expression)) {
|
||||
const text = expression.text;
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(expression, text, `JSX 属性包含需要国际化的内容: ${node.name.getText()}={'${text}'}`, 9301);
|
||||
}
|
||||
}
|
||||
// 检查模板字面量(无替换)
|
||||
else if (ts.isNoSubstitutionTemplateLiteral(expression)) {
|
||||
const text = expression.text;
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(expression, text, `JSX 属性包含需要国际化的内容: ${node.name.getText()}={\`${text}\`}`, 9301);
|
||||
}
|
||||
}
|
||||
// 检查模板表达式(有替换) - 可选的更严格检查
|
||||
else if (ts.isTemplateExpression(expression)) {
|
||||
// 检查模板头部
|
||||
const headText = expression.head.text;
|
||||
if (needsI18n(headText)) {
|
||||
addDiagnostic(expression.head, headText, `JSX 属性的模板字符串包含需要国际化的内容: ${node.name.getText()}`, 9302);
|
||||
}
|
||||
// 检查每个模板片段
|
||||
expression.templateSpans.forEach(span => {
|
||||
const spanText = span.literal.text;
|
||||
if (needsI18n(spanText)) {
|
||||
addDiagnostic(span.literal, spanText, `JSX 属性的模板字符串包含需要国际化的内容: ${node.name.getText()}`, 9302);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
}
|
||||
return diagnostics;
|
||||
};
|
||||
// 信息收集
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
|
|
@ -2572,7 +2695,16 @@ function performCustomChecks(pwd, program, typeChecker, customConfig) {
|
|||
checkDirectContextCalls(); // 检查直接调用
|
||||
checkIndirectCalls(); // 检查间接调用(使用缓存的调用列表)
|
||||
const i18nDiagnostics = checkI18nKeys(pwd, tFunctionSymbolCache, program, typeChecker);
|
||||
return diagnostics.concat(i18nDiagnostics);
|
||||
let jsxLiteralDiagnostics = [];
|
||||
const enableJsxLiteralCheck = customConfig.locale?.checkJsxLiterals === true;
|
||||
if (enableJsxLiteralCheck) {
|
||||
console.log(`Custom JSX literal checks are ${colors.green + 'enabled' + colors.reset}.`);
|
||||
jsxLiteralDiagnostics = checkJsxLiterals(pwd, program, typeChecker, customConfig);
|
||||
}
|
||||
else {
|
||||
console.log(`Custom JSX literal checks are ${colors.gray + 'disabled' + colors.reset}.`);
|
||||
}
|
||||
return diagnostics.concat(i18nDiagnostics).concat(jsxLiteralDiagnostics);
|
||||
}
|
||||
const loadI18nData = (dirPath) => {
|
||||
// 尝试加载 LOCALE_FILE_NAMES 中的文件
|
||||
|
|
|
|||
|
|
@ -281,31 +281,47 @@ function pushStatementIntoSchemaAst(moduleName: string, statement: ts.Statement,
|
|||
*/
|
||||
function checkActionDefNameConsistent(filename: string, actionDefNode: ts.VariableDeclaration) {
|
||||
const { name, type } = actionDefNode;
|
||||
assert(ts.isTypeReferenceNode(type!), "ActionDef应该是一个类型引用");
|
||||
assert(ts.isTypeReferenceNode(type!),
|
||||
`「${filename}」中的 ActionDef 定义错误:必须是类型引用,如 ActionDef<CreateAction, CreateState>`);
|
||||
const { typeArguments } = type!;
|
||||
assert(typeArguments!.length === 2);
|
||||
assert(typeArguments!.length === 2,
|
||||
`「${filename}」中的 ActionDef 定义错误:必须有两个类型参数 <Action, State>`);
|
||||
const [actionNode, stateNode] = typeArguments!;
|
||||
|
||||
assert(ts.isIdentifier(name), `文件${filename}中的ActionDef${(<ts.Identifier>name).text}不是一个有效的变量`);
|
||||
assert(name.text.endsWith('ActionDef'), `文件${filename}中的ActionDef${name.text}未以ActionDef结尾`);
|
||||
assert(ts.isTypeReferenceNode(actionNode) && ts.isTypeReferenceNode(stateNode), `文件${filename}中的ActionDef${name.text}类型声明中的action和state非法`);
|
||||
assert(ts.isIdentifier(actionNode.typeName) && ts.isIdentifier(stateNode.typeName));
|
||||
assert(actionNode.typeName.text.endsWith('Action'), `文件${filename}中的ActionDef${name.text}所引用的Action${actionNode.typeName}未以Action结尾`);
|
||||
assert(stateNode.typeName.text.endsWith('State'), `文件${filename}中的ActionDef${name.text}所引用的State${stateNode.typeName}未以State结尾`);
|
||||
assert(ts.isIdentifier(name),
|
||||
`「${filename}」中的 ActionDef「${(<ts.Identifier>name).text}」不是有效的变量名`);
|
||||
assert(name.text.endsWith('ActionDef'),
|
||||
`「${filename}」中的变量「${name.text}」命名错误:ActionDef 变量必须以 'ActionDef' 结尾`);
|
||||
assert(ts.isTypeReferenceNode(actionNode) && ts.isTypeReferenceNode(stateNode),
|
||||
`「${filename}」中的「${name.text}」类型参数错误:Action 和 State 必须是类型引用`);
|
||||
assert(ts.isIdentifier(actionNode.typeName) && ts.isIdentifier(stateNode.typeName),
|
||||
`「${filename}」中的「${name.text}」类型参数错误:Action 和 State 必须是简单标识符`);
|
||||
assert(actionNode.typeName.text.endsWith('Action'),
|
||||
`「${filename}」中的「${name.text}」引用的 Action 类型「${actionNode.typeName.text}」命名错误:必须以 'Action' 结尾`);
|
||||
assert(stateNode.typeName.text.endsWith('State'),
|
||||
`「${filename}」中的「${name.text}」引用的 State 类型「${stateNode.typeName.text}」命名错误:必须以 'State' 结尾`);
|
||||
|
||||
const adfName = name.text.slice(0, name.text.length - 9);
|
||||
const aName = actionNode.typeName.text.slice(0, actionNode.typeName.text.length - 6);
|
||||
const sName = stateNode.typeName.text.slice(0, stateNode.typeName.text.length - 5);
|
||||
|
||||
assert(adfName === aName && aName === sName, `文件${filename}中的ActionDef${name.text}中ActionDef, Action和State的命名规则不一致, 需要
|
||||
${adfName}ActionDef, ${adfName}Action, ${adfName}State`);
|
||||
assert(adfName === aName && aName === sName,
|
||||
`「${filename}」中的「${name.text}」命名不一致。\n` +
|
||||
`要求:ActionDef、Action、State 的前缀必须相同\n` +
|
||||
`当前:${name.text}, ${actionNode.typeName.text}, ${stateNode.typeName.text}\n` +
|
||||
`应为:${adfName}ActionDef, ${adfName}Action, ${adfName}State`);
|
||||
}
|
||||
|
||||
|
||||
function checkStringLiteralLegal(filename: string, obj: string, text: string, ele: ts.TypeNode) {
|
||||
assert(ts.isLiteralTypeNode(ele) && ts.isStringLiteral(ele.literal), `${filename}中引用的${obj} ${text}中存在不是stringliteral的类型`);
|
||||
assert(!ele.literal.text.includes('$'), `${filename}中引用的action${text}中的${obj}「${ele.literal.text}」包含非法字符$`);
|
||||
assert(ele.literal.text.length > 0, `${filename}中引用的action${text}中的${obj}「${ele.literal.text}」长度非法`);
|
||||
assert(ele.literal.text.length < STRING_LITERAL_MAX_LENGTH, `${filename}中引用的${obj} ${text}中的「${ele.literal.text}」长度过长`);
|
||||
assert(ts.isLiteralTypeNode(ele) && ts.isStringLiteral(ele.literal),
|
||||
`「${filename}」中的 ${obj}「${text}」类型错误:必须是字符串字面量类型,如 'create' | 'update'`);
|
||||
assert(!ele.literal.text.includes('$'),
|
||||
`「${filename}」中的 ${obj}「${text}」包含非法字符:「${ele.literal.text}」中不能包含 '$' 字符`);
|
||||
assert(ele.literal.text.length > 0,
|
||||
`「${filename}」中的 ${obj}「${text}」不能为空字符串`);
|
||||
assert(ele.literal.text.length < STRING_LITERAL_MAX_LENGTH,
|
||||
`「${filename}」中的 ${obj}「${text}」的值「${ele.literal.text}」长度超限。\n` +
|
||||
`当前长度:${ele.literal.text.length},最大允许:${STRING_LITERAL_MAX_LENGTH}`);
|
||||
return ele.literal.text;
|
||||
}
|
||||
|
||||
|
|
@ -576,14 +592,18 @@ function dealWithActionTypeNode(moduleName: string, filename: string, actionType
|
|||
ele => !!ele
|
||||
);
|
||||
assert(intersection(actionNames, RESERVED_ACTION_NAMES).length === 0,
|
||||
`${filename}中的Action命名不能是「${RESERVED_ACTION_NAMES.join(',')}」之一`);
|
||||
`「${filename}」中的 Action 命名冲突:不能使用保留名称。\n` +
|
||||
`保留名称:${RESERVED_ACTION_NAMES.join(', ')}\n` +
|
||||
`冲突的名称:${intersection(actionNames, RESERVED_ACTION_NAMES).join(', ')}`);
|
||||
|
||||
actionTypeNode.types.forEach(
|
||||
ele => {
|
||||
if (ts.isTypeReferenceNode(ele)) {
|
||||
// 这里递归了类型定义,去查找所有的action字符串
|
||||
const actionStrings = tryGetStringLiteralValues(moduleName, filename, 'action', ele, program);
|
||||
assert(actionStrings.length > 0, `${filename}中的Action所引用的Type定义为空`);
|
||||
assert(actionStrings.length > 0,
|
||||
`「${filename}」中的 Action 类型「${(<ts.Identifier>ele.typeName).text}」定义为空,必须至少包含一个 action`);
|
||||
|
||||
actionTexts.push(...actionStrings);
|
||||
}
|
||||
else {
|
||||
|
|
@ -595,8 +615,10 @@ function dealWithActionTypeNode(moduleName: string, filename: string, actionType
|
|||
}
|
||||
else if (ts.isTypeReferenceNode(actionTypeNode)) {
|
||||
if (ts.isIdentifier(actionTypeNode.typeName)) {
|
||||
assert(!RESERVED_ACTION_NAMES.includes(actionTypeNode.typeName.text),
|
||||
`${filename}中的Action命名不能是「${RESERVED_ACTION_NAMES.join(',')}」之一`);
|
||||
assert(intersection([actionTypeNode.typeName.text], RESERVED_ACTION_NAMES).length === 0,
|
||||
`「${filename}」中的 Action 命名冲突:不能使用保留名称。\n` +
|
||||
`保留名称:${RESERVED_ACTION_NAMES.join(', ')}\n` +
|
||||
`冲突的名称:${intersection([actionTypeNode.typeName.text], RESERVED_ACTION_NAMES).join(', ')}`);
|
||||
}
|
||||
const actionStrings = tryGetStringLiteralValues(moduleName, filename, 'action', actionTypeNode, program);
|
||||
assert(actionStrings.length > 0, `${filename}中的Action定义为空`);
|
||||
|
|
@ -611,8 +633,11 @@ function dealWithActionTypeNode(moduleName: string, filename: string, actionType
|
|||
const ActionDict = {};
|
||||
actionTexts.forEach(
|
||||
(action) => {
|
||||
assert(action.length <= STRING_LITERAL_MAX_LENGTH, `${filename}中的Action「${action}」命名长度大于${STRING_LITERAL_MAX_LENGTH}`);
|
||||
assert(/^[a-z][a-z|A-Z]+$/.test(action), `${filename}中的Action「${action}」命名不合法,必须以小字字母开头且只能包含字母`)
|
||||
assert(action.length <= STRING_LITERAL_MAX_LENGTH,
|
||||
`「${filename}」中的 Action「${action}」命名长度超限。\n` +
|
||||
`当前长度:${action.length},最大允许:${STRING_LITERAL_MAX_LENGTH}`);
|
||||
assert(/^[a-z][a-zA-Z]+$/.test(action),
|
||||
`「${filename}」中的 Action「${action}」命名格式错误:必须以小写字母开头且只能包含字母`);
|
||||
if (ActionDict.hasOwnProperty(action)) {
|
||||
throw new Error(`文件${filename}中,Action定义上的【${action}】动作存在同名`);
|
||||
}
|
||||
|
|
@ -689,7 +714,7 @@ function dealWithActionDefInitializer(moduleName: string, initializer: ts.Expres
|
|||
if (existsSync(jsFileName)) {
|
||||
try {
|
||||
const jsContent = require('fs').readFileSync(jsFileName, 'utf-8');
|
||||
|
||||
|
||||
// 获取原始的导出名称(处理 as 别名的情况)
|
||||
// 如果有 propertyName,说明使用了 as,应该使用 propertyName
|
||||
// 否则使用 name
|
||||
|
|
@ -903,15 +928,21 @@ function checkLocaleExpressionPropertyExists(root: ts.ObjectLiteralExpression, a
|
|||
} */
|
||||
|
||||
function checkNameLegal(filename: string, attrName: string, upperCase?: boolean) {
|
||||
assert(attrName.length <= ENTITY_NAME_MAX_LENGTH, `文件「${filename}」:「${attrName}」的名称定义过长,不能超过「${ENTITY_NAME_MAX_LENGTH}」长度`);
|
||||
assert(attrName.length <= ENTITY_NAME_MAX_LENGTH,
|
||||
`「${filename}」中的名称「${attrName}」长度超限。\n` +
|
||||
`当前长度:${attrName.length},最大允许:${ENTITY_NAME_MAX_LENGTH}`);
|
||||
|
||||
if (upperCase) {
|
||||
assert(/[A-Z][a-z|A-Z|0-9]+/i.test(attrName), `文件「${filename}」:「${attrName}」的名称必须以大写字母开始,且只能包含字母和数字`);
|
||||
assert(/[A-Z][a-z|A-Z|0-9]+/i.test(attrName),
|
||||
`「${filename}」中的名称「${attrName}」格式错误:必须以大写字母开头,只能包含字母和数字(如:UserProfile)`);
|
||||
}
|
||||
else if (upperCase === false) {
|
||||
assert(/[a-z][a-z|A-Z|0-9]+/i.test(attrName), `文件「${filename}」:「${attrName}」的名称必须以小写字母开始,且只能包含字母和数字`);
|
||||
assert(/[a-z][a-z|A-Z|0-9]+/i.test(attrName),
|
||||
`「${filename}」中的名称「${attrName}」格式错误:必须以小写字母开头,只能包含字母和数字(如:userName)`);
|
||||
}
|
||||
else {
|
||||
assert(/[a-z|A-Z][a-z|A-Z|0-9]+/i.test(attrName), `文件「${filename}」:「${attrName}」的名称必须以字母开始,且只能包含字母和数字`);
|
||||
assert(/[a-z|A-Z][a-z|A-Z|0-9]+/i.test(attrName),
|
||||
`「${filename}」中的名称「${attrName}」格式错误:必须以字母开头,只能包含字母和数字`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1136,7 +1167,9 @@ function analyzeSchemaDefinition(
|
|||
let toLog = false;
|
||||
const extendsFrom: string[] = [];
|
||||
const { members, heritageClauses } = node;
|
||||
assert(heritageClauses, `「${filename}」中的Schema定义需要继承EntityShape或者其他Entity`);
|
||||
assert(heritageClauses,
|
||||
`「${filename}」中的 Schema 定义错误:必须继承 EntityShape 或其他实体。\n` +
|
||||
`示例:export interface Schema extends EntityShape { ... }`);
|
||||
|
||||
const heritagedResult = heritageClauses.map(
|
||||
(clause) => {
|
||||
|
|
@ -1148,7 +1181,9 @@ function analyzeSchemaDefinition(
|
|||
}
|
||||
|
||||
// 从其它文件的Schema类继承,这里需要将所继承的对象的属性进行访问并展开
|
||||
assert(referencedSchemas.includes(expression.text), `「${filename}」中的Schema继承了未定义的实体「${expression.text}」`);
|
||||
assert(referencedSchemas.includes(expression.text),
|
||||
`「${filename}」中的 Schema 继承了未导入的实体「${expression.text}」。\n` +
|
||||
`提示:请先导入该实体:import { Schema as ${expression.text} } from '...'`);
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(expression);
|
||||
let declaration = symbol?.getDeclarations()![0]!;
|
||||
|
|
@ -1200,12 +1235,31 @@ function analyzeSchemaDefinition(
|
|||
else if (type.typeName.text === 'Array') {
|
||||
// 这是一对多的反向指针的引用,需要特殊处理
|
||||
const { typeArguments } = type;
|
||||
assert(typeArguments!.length === 1
|
||||
&& ts.isTypeReferenceNode(typeArguments![0])
|
||||
&& ts.isIdentifier(typeArguments![0].typeName)
|
||||
&& referencedSchemas.includes(typeArguments![0].typeName.text),
|
||||
`「${filename}」非法的属性定义「${attrName}」`);
|
||||
const reverseEntity = typeArguments![0].typeName.text;
|
||||
assert(
|
||||
typeArguments && typeArguments.length === 1,
|
||||
`「${filename}」的属性「${attrName}」:Array 类型必须有且仅有一个类型参数`
|
||||
);
|
||||
const elementType = typeArguments[0];
|
||||
assert(
|
||||
ts.isTypeReferenceNode(elementType),
|
||||
`「${filename}」的属性「${attrName}」:Array<${elementType.getText()}> 的元素类型必须是实体引用,用于记录反向指针关系。\n` +
|
||||
`提示:\n` +
|
||||
` - 如需存储字符串数组,请使用 string[] 类型或者自定义类型存储 JSON:tags?: string[]\n` +
|
||||
` - 如需关联实体,请使用实体类型:attachments?: Array<ExtraFile>`
|
||||
);
|
||||
assert(
|
||||
ts.isIdentifier(elementType.typeName),
|
||||
`「${filename}」的属性「${attrName}」:Array 的元素类型必须是简单标识符`
|
||||
);
|
||||
const elementTypeName = elementType.typeName.text;
|
||||
assert(
|
||||
referencedSchemas.includes(elementTypeName),
|
||||
`「${filename}」的属性「${attrName}」:Array<${elementTypeName}> 中的 ${elementTypeName} 不是有效的实体引用。\n` +
|
||||
`提示:\n` +
|
||||
` - 请确保已导入该实体:import { Schema as ${elementTypeName} } from '...';\n` +
|
||||
` - 或改用 Object 类型或者自定义类型存储 JSON 数据`
|
||||
);
|
||||
const reverseEntity = elementTypeName;
|
||||
if (ReversePointerRelations[reverseEntity]) {
|
||||
if (!ReversePointerRelations[reverseEntity].includes(moduleName)) {
|
||||
ReversePointerRelations[reverseEntity].push(moduleName);
|
||||
|
|
@ -1403,7 +1457,9 @@ function analyzeReferenceSchemaFile(
|
|||
}
|
||||
});
|
||||
|
||||
assert(result, `「${filename}」中没有找到Schema定义`);
|
||||
assert(result,
|
||||
`「${filename}」中缺少 Schema 定义。\n` +
|
||||
`提示:每个实体文件必须包含 'export interface Schema extends EntityShape { ... }'`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -2802,7 +2858,9 @@ function constructFilter(statements: Array<ts.Statement>, entity: string) {
|
|||
}
|
||||
else {
|
||||
// 此时应当是引用本地定义的shape
|
||||
assert(type, `${entity}中的属性${name.toString()}有非法的属性类型定义`);
|
||||
assert(type,
|
||||
`「${entity}」的属性「${name.toString()}」缺少类型定义。\n` +
|
||||
`提示:每个属性都必须明确指定类型,如:name: String<32>`);
|
||||
members.push(
|
||||
factory.createPropertySignature(
|
||||
undefined,
|
||||
|
|
@ -7670,7 +7728,9 @@ export function constructAttributes(entity: string): ts.PropertyAssignment[] {
|
|||
if (ts.isUnionTypeNode(type!)) {
|
||||
if (ts.isLiteralTypeNode(type.types[0])) {
|
||||
if (ts.isStringLiteral(type.types[0].literal)) {
|
||||
assert(enumAttributes && enumAttributes[(<ts.Identifier>name).text], `${entity}对象中的${(<ts.Identifier>name).text}属性没有定义enumAttributes`);
|
||||
assert(enumAttributes && enumAttributes[(<ts.Identifier>name).text],
|
||||
`「${entity}」的属性「${(<ts.Identifier>name).text}」缺少枚举值定义。\n` +
|
||||
`提示:枚举类型属性需要在 enumAttributes 中定义可选值`);
|
||||
attrAssignments.push(
|
||||
factory.createPropertyAssignment(
|
||||
'type',
|
||||
|
|
@ -7687,7 +7747,8 @@ export function constructAttributes(entity: string): ts.PropertyAssignment[] {
|
|||
);
|
||||
}
|
||||
else {
|
||||
assert(ts.isNumericLiteral(type.types[0].literal), `${entity}对象中的${type.types[0].literal.getText()}属性不是数字`);
|
||||
assert(ts.isNumericLiteral(type.types[0].literal),
|
||||
`「${entity}」的属性类型错误:「${type.types[0].literal.getText()}」不是有效的数字字面量`);
|
||||
attrAssignments.push(
|
||||
factory.createPropertyAssignment(
|
||||
factory.createIdentifier("type"),
|
||||
|
|
@ -7739,7 +7800,8 @@ export function constructAttributes(entity: string): ts.PropertyAssignment[] {
|
|||
);
|
||||
}
|
||||
else {
|
||||
assert(ts.isNumericLiteral(type.literal), `${entity}对象中的${(<ts.Identifier>name).text}属性不是数字`);
|
||||
assert(ts.isNumericLiteral(type.literal),
|
||||
`「${entity}」的属性「${(<ts.Identifier>name).text}」类型错误:必须是数字字面量类型`);
|
||||
attrAssignments.push(
|
||||
factory.createPropertyAssignment(
|
||||
factory.createIdentifier("type"),
|
||||
|
|
|
|||
|
|
@ -240,6 +240,8 @@ interface OakBuildChecksConfig {
|
|||
tFunctionModules?: string[]; // t 函数所在模块,默认 ['oak-frontend-base']
|
||||
checkTemplateLiterals?: boolean; // 是否检查模板字符串中的 i18n 键,默认禁用
|
||||
warnStringKeys?: boolean; // 是否警告字符串形式的 i18n 键,默认禁用
|
||||
checkJsxLiterals?: boolean; // 是否检查 JSX 中的字面量文本
|
||||
jsxLiteralPattern?: string; // 用于匹配需要国际化的文本模式,默认匹配中文
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3175,6 +3177,186 @@ export function performCustomChecks(
|
|||
return diagnostics;
|
||||
}
|
||||
|
||||
const checkJsxLiterals = (
|
||||
pwd: string,
|
||||
program: ts.Program,
|
||||
typeChecker: ts.TypeChecker,
|
||||
customConfig: OakBuildChecksConfig
|
||||
): CustomDiagnostic[] => {
|
||||
const diagnostics: CustomDiagnostic[] = [];
|
||||
|
||||
// 默认匹配中文字符的正则
|
||||
const defaultPattern = /[\u4e00-\u9fa5]/;
|
||||
const patternStr = customConfig.locale?.jsxLiteralPattern;
|
||||
const pattern = patternStr ? new RegExp(patternStr) : defaultPattern;
|
||||
|
||||
const addDiagnostic = (
|
||||
node: ts.Node,
|
||||
text: string,
|
||||
messageText: string,
|
||||
code: number
|
||||
): void => {
|
||||
const sourceFile = node.getSourceFile();
|
||||
diagnostics.push({
|
||||
file: sourceFile,
|
||||
start: node.getStart(sourceFile),
|
||||
length: node.getWidth(sourceFile),
|
||||
messageText,
|
||||
category: ts.DiagnosticCategory.Warning,
|
||||
code,
|
||||
reason: 'HardcodedJsxText',
|
||||
reasonDetails: [
|
||||
`✘ 发现硬编码文本: "${text}"`,
|
||||
'建议: 使用 t() 函数进行国际化处理'
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
// 检查文本是否需要国际化
|
||||
const needsI18n = (text: string): boolean => {
|
||||
// 移除前后空白
|
||||
const trimmed = text.trim();
|
||||
|
||||
// 忽略空字符串
|
||||
if (trimmed.length === 0) return false;
|
||||
|
||||
// 忽略纯数字
|
||||
if (/^\d+$/.test(trimmed)) return false;
|
||||
|
||||
// 忽略纯标点符号和特殊字符
|
||||
if (/^[^\w\u4e00-\u9fa5]+$/.test(trimmed)) return false;
|
||||
|
||||
// 匹配需要国际化的模式
|
||||
return pattern.test(trimmed);
|
||||
};
|
||||
|
||||
// 遍历所有源文件
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
// 跳过声明文件
|
||||
if (sourceFile.isDeclarationFile) continue;
|
||||
|
||||
// 只检查 tsx 文件
|
||||
if (!sourceFile.fileName.endsWith('.tsx')) continue;
|
||||
|
||||
// 检查是否在检查范围内
|
||||
if (!isFileInCheckScope(pwd, sourceFile.fileName, customConfig)) continue;
|
||||
|
||||
const visit = (node: ts.Node): void => {
|
||||
// 检查 JsxText 节点
|
||||
if (ts.isJsxText(node)) {
|
||||
// 检查是否有忽略注释
|
||||
if (hasIgnoreComment(node, sourceFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = node.getText(sourceFile);
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(
|
||||
node,
|
||||
text.trim(),
|
||||
`JSX 文本包含需要国际化的内容: "${text.trim()}"`,
|
||||
9300
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 JsxAttribute 节点
|
||||
if (ts.isJsxAttribute(node)) {
|
||||
// 检查是否有忽略注释
|
||||
if (hasIgnoreComment(node, sourceFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initializer = node.initializer;
|
||||
|
||||
if (!initializer) {
|
||||
ts.forEachChild(node, visit);
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况1: 直接字符串字面量 desc="text"
|
||||
if (ts.isStringLiteral(initializer)) {
|
||||
const text = initializer.text;
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(
|
||||
initializer,
|
||||
text,
|
||||
`JSX 属性包含需要国际化的内容: ${node.name.getText()}="${text}"`,
|
||||
9301
|
||||
);
|
||||
}
|
||||
}
|
||||
// 情况2: JSX 表达式 desc={'text'} 或 desc={`text`}
|
||||
else if (ts.isJsxExpression(initializer)) {
|
||||
const expression = initializer.expression;
|
||||
|
||||
if (!expression) {
|
||||
ts.forEachChild(node, visit);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查字符串字面量
|
||||
if (ts.isStringLiteral(expression)) {
|
||||
const text = expression.text;
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(
|
||||
expression,
|
||||
text,
|
||||
`JSX 属性包含需要国际化的内容: ${node.name.getText()}={'${text}'}`,
|
||||
9301
|
||||
);
|
||||
}
|
||||
}
|
||||
// 检查模板字面量(无替换)
|
||||
else if (ts.isNoSubstitutionTemplateLiteral(expression)) {
|
||||
const text = expression.text;
|
||||
if (needsI18n(text)) {
|
||||
addDiagnostic(
|
||||
expression,
|
||||
text,
|
||||
`JSX 属性包含需要国际化的内容: ${node.name.getText()}={\`${text}\`}`,
|
||||
9301
|
||||
);
|
||||
}
|
||||
}
|
||||
// 检查模板表达式(有替换) - 可选的更严格检查
|
||||
else if (ts.isTemplateExpression(expression)) {
|
||||
// 检查模板头部
|
||||
const headText = expression.head.text;
|
||||
if (needsI18n(headText)) {
|
||||
addDiagnostic(
|
||||
expression.head,
|
||||
headText,
|
||||
`JSX 属性的模板字符串包含需要国际化的内容: ${node.name.getText()}`,
|
||||
9302
|
||||
);
|
||||
}
|
||||
|
||||
// 检查每个模板片段
|
||||
expression.templateSpans.forEach(span => {
|
||||
const spanText = span.literal.text;
|
||||
if (needsI18n(spanText)) {
|
||||
addDiagnostic(
|
||||
span.literal,
|
||||
spanText,
|
||||
`JSX 属性的模板字符串包含需要国际化的内容: ${node.name.getText()}`,
|
||||
9302
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
};
|
||||
|
||||
// 信息收集
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
|
|
@ -3204,7 +3386,16 @@ export function performCustomChecks(
|
|||
|
||||
const i18nDiagnostics = checkI18nKeys(pwd, tFunctionSymbolCache, program, typeChecker);
|
||||
|
||||
return diagnostics.concat(i18nDiagnostics);
|
||||
let jsxLiteralDiagnostics: CustomDiagnostic[] = [];
|
||||
const enableJsxLiteralCheck = customConfig.locale?.checkJsxLiterals === true;
|
||||
if (enableJsxLiteralCheck) {
|
||||
console.log(`Custom JSX literal checks are ${colors.green + 'enabled' + colors.reset}.`);
|
||||
jsxLiteralDiagnostics = checkJsxLiterals(pwd, program, typeChecker, customConfig);
|
||||
} else {
|
||||
console.log(`Custom JSX literal checks are ${colors.gray + 'disabled' + colors.reset}.`);
|
||||
}
|
||||
|
||||
return diagnostics.concat(i18nDiagnostics).concat(jsxLiteralDiagnostics);
|
||||
}
|
||||
|
||||
const loadI18nData = (dirPath: string): Record<string, string> | null => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue