chameleon-linter
Version:
cml规范校验工具
227 lines (207 loc) • 7.28 kB
JavaScript
const traverse = require('@babel/traverse')['default'];
const utils = require('../utils');
/**
* 获取接口定义
*
* @param {Object} ast ast
* @return {Object} 分析结果
*/
const getInterfaces = (ast) => {
let result = {
name: '',
properties: {}
};
ast.program.body.forEach(function (node) {
if (node.type == 'InterfaceDeclaration') {
let interfaceName = node.id.name;
result.name = interfaceName;
result.loc = {
line: node.id.loc.start.line,
column: node.id.loc.start.column
};
node.body.properties.map((property) => {
result.properties[property.key.name] = {
type: property.value.type.replace(/TypeAnnotation/g, ''),
line: property.key.loc.start.line,
column: property.key.loc.start.column
};
});
}
});
return result;
};
/**
* 获取类定义
* @param {Object} ast ast
* @param {Object} isComp a flag indentify whether the ast is component or an interface portion
* @return {Object} 类定义
*/
const getClass = (ast, isComp) => {
return isComp ? getCompClassDef(ast) : getInterfacePortionClassDef(ast);
};
function getCompClassDef(ast) {
let classes = [];
let clazz = {
interfaces: [],
properties: [],
events: [],
methods: []
};
traverse(ast, {
ClassDeclaration(path) {
// 接口
if (path.node['implements']) {
path.node['implements'].forEach(implament => {
clazz.interfaces.push(implament.id.name);
});
}
path.node.body.body.forEach(define => {
if (define.key.name == 'props') {
define.value.properties.forEach(property => {
clazz.properties.push(property.key.name);
});
}
else if (define.key.name == 'methods') {
define.value.properties.filter(property => {
return property.type === 'ObjectMethod';
}).forEach(property => {
clazz.methods.push(property.key.name);
});
}
});
classes.push(clazz);
},
MemberExpression(path) {
if (!path.node.computed && path.get('object').isThisExpression() && path.get('property').isIdentifier()) {
if (path.node.property.name === '$cmlEmit') {
let parentNode = path.findParent(path => path.isCallExpression());
if (parentNode && parentNode.get('arguments')) {
let event = null;
let nameArg = parentNode.get('arguments')[0];
if (nameArg.isStringLiteral()) {
event = {
event: nameArg.node.value,
line: nameArg.node.loc.start.line,
column: nameArg.node.loc.start.column
};
} else if (nameArg.isIdentifier()) {
let argBinding = nameArg.scope.getBinding(nameArg.node.name);
let possibleInit = argBinding ? argBinding.path.node.init : null;
// For now, we only check just one jump along its scope chain.
if (possibleInit && possibleInit.type === 'StringLiteral') {
event = {
event: possibleInit.value,
line: nameArg.node.loc.start.line,
column: nameArg.node.loc.start.column
};
}
}
if (event) {
clazz.methods.push(event.event);
clazz.events.push(event);
}
}
}
}
}
});
return classes;
}
function getInterfacePortionClassDef(ast) {
let classes = [];
traverse(ast, {
enter(path) {
if (path.node.type == 'ClassDeclaration') {
let clazz = {
interfaces: [],
properties: [],
events: [],
methods: []
};
// 接口
if (path.node['implements']) {
path.node['implements'].forEach(implament => {
clazz.interfaces.push(implament.id.name);
});
}
path.node.body.body.forEach(define => {
if (define.type == 'ClassProperty') {
clazz.properties.push(define.key.name);
} else if (define.type === 'ClassMethod') {
define.key.name && (clazz.methods.push(define.key.name));
}
});
classes.push(clazz);
}
}
});
return classes;
}
/**
* 校验接口与脚本
*
* @param {Object} interfaceAst 接口ast
* @return {Array} 数组
*/
const checkScript = async (result) => {
let validPlatforms = Object.keys(result)
.filter(platform => {
return platform && (!~['json', 'template', 'style', 'script'].indexOf(platform));
})
.filter(platform => {
return platform && (platform != 'interface');
});
// add a script type for multi-file components.
result['interface'] && validPlatforms.concat('script').forEach(platform => {
let script;
let isComp = (platform === 'script');
if (result[platform] && result[platform].ast) {
script = result[platform];
}
if (result['interface'] && result['interface'].ast && script && script.ast) {
const interfaceDefine = getInterfaces(result['interface'].ast);
const classDefines = getClass(script.ast, isComp);
classDefines.forEach(clazz => {
let define = null;
clazz.interfaces.forEach(interfaceName => {
define = interfaceDefine.name === interfaceName ? interfaceDefine.properties : null;
if (!define) {
result['interface'].messages.push({
msg: `The implement class name: "${interfaceName}" used in file: "${utils.toSrcPath(script.file)}" doesn\'t match the name defined in it\'s interface file: "${utils.toSrcPath(result['interface'].file)}"`
});
return;
}
for (let key of Object.keys(define)) {
if ((define[key] && define[key].type == 'Generic') && clazz.properties.indexOf(key) == -1) {
result['interface'].messages.push({
line: define[key].line,
column: define[key].column,
token: key,
msg: `interface property "${key}" is not defined for platform ${script.platform} in file "${utils.toSrcPath(script.file)}"`
});
}
else if ((define[key] && define[key].type == 'Function' && clazz.methods.indexOf(key) === -1)) {
result['interface'].messages.push({
line: define[key].line,
column: define[key].column,
token: key,
msg: `interface method "${key}" is not defined for platform ${script.platform} in file "${utils.toSrcPath(script.file)}"`
});
}
}
});
define && clazz.events.forEach(event => {
if (!define[event.event] || (define[event.event] && (define[event.event].type != 'Function'))) {
script.messages.push({
line: event.line,
column: event.column,
token: event.event,
msg: 'event "' + event.event + '" is not defined in interface file "' + utils.toSrcPath(result['interface'].file) + '"'
});
}
});
});
}
});
};
module.exports = checkScript;