UNPKG

chameleon-linter

Version:

cml规范校验工具

227 lines (207 loc) 7.28 kB
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;