diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f47ae3392ee32f701f3cfc41bd9a94d1767290f..00d439678f2daada46f00deb386ef2ad853d0a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ - 支持web weex可配置是否包裹组件 ## [0.3.3-alpha.2] -### Bug Fixes +### Bug Fixes * 修复 weex dev模式liveload失效 老项目如果修复,还需要升级项目中两个npm包如下: diff --git a/packages/chameleon-linter/checkers/script.js b/packages/chameleon-linter/checkers/script.js index 617dfe866c55e49cb9a38a993fd64173b1df0bb9..ad9b8b5ad8e9961043e67ecc0dd39efcbe443ef9 100644 --- a/packages/chameleon-linter/checkers/script.js +++ b/packages/chameleon-linter/checkers/script.js @@ -1,175 +1,5 @@ const traverse = require('@babel/traverse')['default']; -const deepTraverse = require('traverse'); -const uniq = require('lodash.uniq'); -const config = require('../config'); - -const DEFAULT_TOKENS_MAP = { - WEEX: ['weex', 'global'], - WX: ['wx', 'global'], - BAIDU: ['swan', 'global'], - ALIPAY: ['my', 'global'], - WEB: [ - 'postMessage', 'blur', 'focus', 'close', 'frames', 'self', - 'window', 'parent', 'opener', 'top', 'length', 'closed', - 'location', 'document', 'origin', 'name', 'history', - 'locationbar', 'menubar', 'personalbar', 'scrollbars', - 'statusbar', 'toolbar', 'status', 'frameElement', 'navigator', - 'customElements', 'external', 'screen', 'innerWidth', - 'innerHeight', 'scrollX', 'pageXOffset', 'scrollY', - 'pageYOffset', 'screenX', 'screenY', 'outerWidth', 'outerHeight', - 'devicePixelRatio', 'clientInformation', 'screenLeft', - 'screenTop', 'defaultStatus', 'defaultstatus', 'styleMedia', - 'onanimationend', 'onanimationiteration', 'onanimationstart', - 'onsearch', 'ontransitionend', 'onwebkitanimationend', - 'onwebkitanimationiteration', 'onwebkitanimationstart', - 'onwebkittransitionend', 'isSecureContext', 'onabort', 'onblur', - 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', - 'onclose', 'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag', - 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', - 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', - 'onfocus', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', - 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', - 'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave', - 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', - 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onreset', - 'onresize', 'onscroll', 'onseeked', 'onseeking', 'onselect', 'onstalled', - 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onvolumechange', - 'onwaiting', 'onwheel', 'onauxclick', 'ongotpointercapture', - 'onlostpointercapture', 'onpointerdown', 'onpointermove', - 'onpointerup', 'onpointercancel', 'onpointerover', 'onpointerout', - 'onpointerenter', 'onpointerleave', 'onafterprint', 'onbeforeprint', - 'onbeforeunload', 'onhashchange', 'onlanguagechange', 'onmessage', - 'onmessageerror', 'onoffline', 'ononline', 'onpagehide', 'onpageshow', - 'onpopstate', 'onrejectionhandled', 'onstorage', 'onunhandledrejection', - 'onunload', 'performance', 'stop', 'open', 'alert', 'confirm', 'prompt', - 'print', 'requestAnimationFrame', 'cancelAnimationFrame', 'requestIdleCallback', - 'cancelIdleCallback', 'captureEvents', 'releaseEvents', 'getComputedStyle', - 'matchMedia', 'moveTo', 'moveBy', 'resizeTo', 'resizeBy', 'getSelection', 'find', - 'webkitRequestAnimationFrame', 'webkitCancelAnimationFrame', 'fetch', - 'btoa', 'atob', 'createImageBitmap', 'scroll', 'scrollTo', 'scrollBy', - 'onappinstalled', 'onbeforeinstallprompt', 'crypto', 'ondevicemotion', - 'ondeviceorientation', 'ondeviceorientationabsolute', 'indexedDB', - 'webkitStorageInfo', 'sessionStorage', 'localStorage', 'chrome', - 'visualViewport', 'speechSynthesis', 'webkitRequestFileSystem', - 'webkitResolveLocalFileSystemURL', 'openDatabase', 'applicationCache', - 'caches', 'whichAnimationEvent', 'animationendEvent', 'infinity', - 'SETTING', 'AppView', 'ExtensionOptions', 'ExtensionView', 'WebView', - 'iconPath', '_app', '_ZOOM_', 'Feed', 'md5', '$', 'jQuery', 'Search', - 'windmill', 'Lethargy', 'alertTimeOut', 'supportApps', 'lethargyX', - 'lethargyY', 'iView', 'onModuleResLoaded', 'iEditDelete', 'infinityDrag', - 'i', 'array', 'TEMPORARY', 'PERSISTENT', 'addEventListener', - 'removeEventListener', 'dispatchEvent' - ] -}; - -// 这个path是否有需要校验的token -const needCheck = function (path, tokenList) { - let flag = false; - for (let i = 0; i < tokenList.length; i++) { - let token = tokenList[i]; - // 判断一个token是否是一个变量 - if (checkToken(path, token)) { - // 如果是对象中的key是需要忽略的 - flag = token; - break; - } - } - - return flag; -}; - -// 是变量,并且不是对象的key值 -function checkToken(path, token) { - if (path.isIdentifier({ name: token })) { - // 是对象的key值 - // const api = { - // name: weex, - // }; - let isObjectKey = path.parent.type === 'ObjectProperty' && path.parentKey === 'key' - // 对象成员 - /** - * var a = { - * weex: 'a' - * } - * console.log(weex.a); - */ - let isObjectMember = path.parent.type === 'MemberExpression' && path.parentKey === 'property' - - return !(isObjectKey || isObjectMember); - } - else { - return false; - } -} - -const checkGlobal = function (ast, type = 'ALL') { - let tokensMap = DEFAULT_TOKENS_MAP; - const TOKENS_MAP = tokensMap; - type = type.toUpperCase(); - - let tokenList = []; - - if (type === 'ALL') { - // 都要校验 - tokenList = TOKENS_MAP[type]; - } - else { - Object.keys(TOKENS_MAP).forEach(key => { - // 把自身的和All的去掉,其他端的token放进去 - if (key !== type && key !== 'ALL') { - tokenList = tokenList.concat(TOKENS_MAP[key]); - } - }) - // 然后要把自身的全局变量去掉 - for (let i = 0;i < tokenList.length;) { - if (~TOKENS_MAP[type].indexOf(tokenList[i])) { - tokenList.splice(i, 1); - } - else { - i++; - } - } - } - tokenList = uniq(tokenList); - const messages = []; - - traverse(ast, { - enter: (path) => { - // path是一个上下文 - // 需要校验的变量值 - - let token = needCheck(path, tokenList); - - // 如果存在该token - if (token) { - let globalVariable = true; - // 当前作用域 - let next = path.scope; - - while (next) { - // 如果当前作用域存在该变量 就不是全局变量 - if (next.hasOwnBinding(token)) { - globalVariable = false; - break; - } - next = next.parent; - } - - - if (globalVariable && path.parent.type != 'ObjectMethod' && path.parent.type != 'ClassMethod') { - messages.push({ - line: path.node.loc.start.line, - column: path.node.loc.start.column, - token: token, - msg: 'global variable [' + token + '] should not used in this file' - }); - } - } - } - }); - return messages; -} - +const utils = require('../utils'); /** * 获取接口定义 @@ -178,13 +8,20 @@ const checkGlobal = function (ast, type = 'ALL') { * @return {Object} 分析结果 */ const getInterfaces = (ast) => { - let result = {}; + let result = { + name: '', + properties: {} + }; ast.program.body.forEach(function (node) { if (node.type == 'InterfaceDeclaration') { let interfaceName = node.id.name; - result[interfaceName] = {}; + result.name = interfaceName; + result.loc = { + line: node.id.loc.start.line, + column: node.id.loc.start.column + }; node.body.properties.map((property) => { - result[interfaceName][property.key.name] = { + result.properties[property.key.name] = { type: property.value.type.replace(/TypeAnnotation/g, ''), line: property.key.loc.start.line, column: property.key.loc.start.column @@ -197,11 +34,89 @@ const getInterfaces = (ast) => { /** * 获取类定义 - * - * @param {Object} ast ast - * @return {Object} 类定义 + * @param {Object} ast ast + * @param {Object} isComp a flag indentify whether the ast is component or an interface portion + * @return {Object} 类定义 */ -const getClass = (ast) => { +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, { @@ -223,43 +138,12 @@ const getClass = (ast) => { // 参数 path.node.body.body.forEach(define => { - if (define.key.name == 'props') { - define.value.properties.forEach(property => { - clazz.properties.push(property.key.name); - }); + if (define.type == 'ClassProperty') { + clazz.properties.push(define.key.name); } else if (define.type == 'ClassMethod') { clazz.methods.push(define.key.name); } - else { - deepTraverse(define) - .nodes() - .filter(function (item) { - if (item && item.type == 'CallExpression') { - return true; - } - }) - .forEach(function (item) { - - if (item.callee.property && item.callee.property.name == '$cmlEmit') { - let event = {}; - - item.arguments.map((arg) => { - if (arg.value) { - event.line = arg.loc.start.line; - event.column = arg.loc.start.column; - event.event = arg.value; - } - else if (arg.properties) { - event.arguments = arg.properties.map(property => { - return property.key.name; - }); - } - }); - clazz.events.push(event); - } - }); - } }); classes.push(clazz); @@ -268,7 +152,8 @@ const getClass = (ast) => { }); return classes; -}; +} + /** * 校验接口与脚本 @@ -277,76 +162,66 @@ const getClass = (ast) => { * @return {Array} 数组 */ const checkScript = async (result) => { - let script; - let platforms = config.getPlatforms(); - - ['script'].concat(platforms).forEach(item => { - if (result[item] && result[item].ast) { - script = result[item]; - } - }); - - if (!result['interface'] && script) { - let interfaceFile = script.file.replace(new RegExp('\\.(' + platforms.join('|') + ')\\.cml$', 'ig'), '.interface'); - if (/\.interface$/.test(interfaceFile)) { - result['interface'] = { - messages: [{ - msg: 'file: [' + interfaceFile + '] was not found!' - }], - file: interfaceFile - }; + 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); - - classDefines.forEach(clazz => { - clazz.interfaces.forEach(interfaceName => { - let define = interfaceDefine[interfaceName]; - - for (let key of Object.keys(define)) { - if ((define[key] && define[key].type == 'Generic') && clazz.properties.indexOf(key) == -1) { + 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({ - line: define[key].line, - column: define[key].column, - token: key, - msg: 'property [' + key + '] is not found in file [' + script.file + ']' + 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; } - else if ((define[key] && define[key].type == 'Function') && clazz.methods.indexOf(key) == -1) { - platforms.forEach(platform => { - if (result[platform]) { - result['interface'].messages.push({ - line: define[key].line, - column: define[key].column, - token: key, - msg: 'method [' + key + '] is not found in file [' + script.file + ']' - }); - } - }); + 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)}"` + }); + } } - } + }); - clazz.events.forEach(event => { + 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 [' + result['interface'].file + ']' + msg: 'event "' + event.event + '" is not defined in interface file "' + utils.toSrcPath(result['interface'].file) + '"' }); } }); }); - }); - - if (script.platform) { - let messages = checkGlobal(script.ast, script.platform); - script.messages = script.messages.concat(messages); } - } + }); }; diff --git a/packages/chameleon-linter/checkers/template/index.js b/packages/chameleon-linter/checkers/template/index.js index eaa1f11699c889c506216d2431ef97ad55bf92c5..4e2df315d0af4a15babdc115fb8d7862d770b2fa 100644 --- a/packages/chameleon-linter/checkers/template/index.js +++ b/packages/chameleon-linter/checkers/template/index.js @@ -58,7 +58,6 @@ class TemplateChecker { .join('|'); usingProps = `|${usingProps}|`; usingEvents = `|${usingEvents}|`; - debugger props.filter((prop) => usingProps.indexOf('|' + prop.name + '|') === -1).forEach((prop) => { issues.push({ line: prop.pos[0], diff --git a/packages/chameleon-linter/checkers/template/lib/template-ast-parser/rules/template/mustache-node.js b/packages/chameleon-linter/checkers/template/lib/template-ast-parser/rules/template/mustache-node.js index 5545300ecb0a5c2fea574a4ed32bf22668e5053d..12b1edf54f09f26882f5e35e9f42dfd6e2df5eaf 100644 --- a/packages/chameleon-linter/checkers/template/lib/template-ast-parser/rules/template/mustache-node.js +++ b/packages/chameleon-linter/checkers/template/lib/template-ast-parser/rules/template/mustache-node.js @@ -3,7 +3,7 @@ const Tools = require('../../tools'); const mustacheRegex = /{{(.*?)}}/g; module.exports = { - name: 'method-node', + name: 'mustache-node', on: ['cml', 'vue'], filter: { key: 'rawValue', diff --git a/packages/chameleon-linter/checkers/template/lib/template-ast-parser/tools/parse-single-expression.js b/packages/chameleon-linter/checkers/template/lib/template-ast-parser/tools/parse-single-expression.js index 8cd762b9f6f2ae4cfb88f3ff5f0a57061657a0dd..509ec0c59abf596319455b1310c7eaaa47cdbfa5 100644 --- a/packages/chameleon-linter/checkers/template/lib/template-ast-parser/tools/parse-single-expression.js +++ b/packages/chameleon-linter/checkers/template/lib/template-ast-parser/tools/parse-single-expression.js @@ -75,6 +75,14 @@ module.exports.parseSingleExpression = function(expressinoStr = '') { nodes.push(getVarFromIdentifier(path.node.right, isFakeBlock)); } }, + LogicalExpression(path) { + if (path.get('left').isIdentifier()) { + nodes.push(getVarFromIdentifier(path.node.left, isFakeBlock)); + } + if (path.get('right').isIdentifier()) { + nodes.push(getVarFromIdentifier(path.node.right, isFakeBlock)); + } + }, SequenceExpression(path) { path.get('expressions').forEach((ele) => { if (ele.isIdentifier()) { diff --git a/packages/chameleon-linter/config/globalVars.js b/packages/chameleon-linter/config/globalVars.js new file mode 100644 index 0000000000000000000000000000000000000000..d5160974e9c58e06745a2e35c4a9b598b88cdc4a --- /dev/null +++ b/packages/chameleon-linter/config/globalVars.js @@ -0,0 +1,61 @@ +const globalVars = { + WEEX: ['weex', 'global'], + WX: ['wx', 'global'], + BAIDU: ['swan', 'global'], + ALIPAY: ['my', 'global'], + QQ: ['qq', 'global'], + WEB: [ + 'postMessage', 'blur', 'focus', 'close', 'frames', 'self', + 'window', 'parent', 'opener', 'top', 'length', 'closed', + 'location', 'document', 'origin', 'name', 'history', + 'locationbar', 'menubar', 'personalbar', 'scrollbars', + 'statusbar', 'toolbar', 'status', 'frameElement', 'navigator', + 'customElements', 'external', 'screen', 'innerWidth', + 'innerHeight', 'scrollX', 'pageXOffset', 'scrollY', + 'pageYOffset', 'screenX', 'screenY', 'outerWidth', 'outerHeight', + 'devicePixelRatio', 'clientInformation', 'screenLeft', + 'screenTop', 'defaultStatus', 'defaultstatus', 'styleMedia', + 'onanimationend', 'onanimationiteration', 'onanimationstart', + 'onsearch', 'ontransitionend', 'onwebkitanimationend', + 'onwebkitanimationiteration', 'onwebkitanimationstart', + 'onwebkittransitionend', 'isSecureContext', 'onabort', 'onblur', + 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', + 'onclose', 'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag', + 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', + 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', + 'onfocus', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', + 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', + 'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave', + 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', + 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onreset', + 'onresize', 'onscroll', 'onseeked', 'onseeking', 'onselect', 'onstalled', + 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onvolumechange', + 'onwaiting', 'onwheel', 'onauxclick', 'ongotpointercapture', + 'onlostpointercapture', 'onpointerdown', 'onpointermove', + 'onpointerup', 'onpointercancel', 'onpointerover', 'onpointerout', + 'onpointerenter', 'onpointerleave', 'onafterprint', 'onbeforeprint', + 'onbeforeunload', 'onhashchange', 'onlanguagechange', 'onmessage', + 'onmessageerror', 'onoffline', 'ononline', 'onpagehide', 'onpageshow', + 'onpopstate', 'onrejectionhandled', 'onstorage', 'onunhandledrejection', + 'onunload', 'performance', 'stop', 'open', 'alert', 'confirm', 'prompt', + 'print', 'requestAnimationFrame', 'cancelAnimationFrame', 'requestIdleCallback', + 'cancelIdleCallback', 'captureEvents', 'releaseEvents', 'getComputedStyle', + 'matchMedia', 'moveTo', 'moveBy', 'resizeTo', 'resizeBy', 'getSelection', 'find', + 'webkitRequestAnimationFrame', 'webkitCancelAnimationFrame', 'fetch', + 'btoa', 'atob', 'createImageBitmap', 'scroll', 'scrollTo', 'scrollBy', + 'onappinstalled', 'onbeforeinstallprompt', 'crypto', 'ondevicemotion', + 'ondeviceorientation', 'ondeviceorientationabsolute', 'indexedDB', + 'webkitStorageInfo', 'sessionStorage', 'localStorage', 'chrome', + 'visualViewport', 'speechSynthesis', 'webkitRequestFileSystem', + 'webkitResolveLocalFileSystemURL', 'openDatabase', 'applicationCache', + 'caches', 'whichAnimationEvent', 'animationendEvent', 'infinity', + 'SETTING', 'AppView', 'ExtensionOptions', 'ExtensionView', 'WebView', + 'iconPath', '_app', '_ZOOM_', 'Feed', 'md5', '$', 'jQuery', 'Search', + 'windmill', 'Lethargy', 'alertTimeOut', 'supportApps', 'lethargyX', + 'lethargyY', 'iView', 'onModuleResLoaded', 'iEditDelete', 'infinityDrag', + 'i', 'array', 'TEMPORARY', 'PERSISTENT', 'addEventListener', + 'removeEventListener', 'dispatchEvent' + ] +}; + +module.exports = globalVars; diff --git a/packages/chameleon-linter/config/white-list/cml-white-list.js b/packages/chameleon-linter/config/white-list/cml-white-list.js index 51916e8e900f3d8793dc46b6d22d87dce290050e..39b886031bc3363e54403eeeb7619029d29776d4 100644 --- a/packages/chameleon-linter/config/white-list/cml-white-list.js +++ b/packages/chameleon-linter/config/white-list/cml-white-list.js @@ -22,6 +22,7 @@ module.exports = { 'checkbox', 'page', 'router-view', + 'cover-view', 'slot', 'aside', 'col', diff --git a/packages/chameleon-linter/file-spec.js b/packages/chameleon-linter/file-spec.js index 2dfb1c4124e46301edc93da4426f79f052851465..91f7d9bfadbbcaeff06d9a1d6a797bf9d35b8341 100644 --- a/packages/chameleon-linter/file-spec.js +++ b/packages/chameleon-linter/file-spec.js @@ -200,3 +200,4 @@ const checkFileSpecification = async (filepath, filetype) => { module.exports = checkFileSpecification; module.exports.lintCmlFile = lintCmlFile; +module.exports.checkFile = checkFile; diff --git a/packages/chameleon-linter/linters/script.js b/packages/chameleon-linter/linters/script.js index f9290afc4e20723a2d1da63965499505274782be..12a78cf047dd50f19379eb60a4665f18ad25f5a1 100644 --- a/packages/chameleon-linter/linters/script.js +++ b/packages/chameleon-linter/linters/script.js @@ -1,6 +1,8 @@ const parse = require('@babel/parser').parse; const config = require('../config/parser-config'); -const traverse = require('@babel/traverse')["default"]; +const traverse = require('@babel/traverse')['default']; +const globalVars = require('../config/globalVars'); +const uniq = require('lodash.uniq'); const Map = { watch: 'watcher', @@ -8,7 +10,37 @@ const Map = { methods: 'method' }; -const handleProperty = function (property, propertyName, messages) { +function checkArrowFun(path) { + let messages = []; + if (path.node) { + let properties = path.get('value').get('properties'); + switch (path.node.key.name) { + case 'watch': + case 'computed': + case 'methods': + if (properties.forEach) { + (properties || []).forEach(property => { + messages = messages.concat(handleProperty(property, path.node.key.name)); + }); + } + break; + case 'beforeCreate': + case 'created': + case 'beforeMount': + case 'mounted': + case 'beforeDestroy': + case 'destroyed': + messages = messages.concat(handleProperty(path, path.node.key.name)); + break; + default: + break; + } + } + return messages; +} + +function handleProperty(property, propertyName) { + let messages = []; if (property.get('value').isArrowFunctionExpression()) { let node = property.get('key').node; let name = node.name; @@ -19,9 +51,44 @@ const handleProperty = function (property, propertyName, messages) { ? (Map[propertyName] + ' "' + name + '" cannot be used as an arrow function') : ('lifecycle hook "' + name + '" cannot be used as an arrow function') }); - } -}; + return messages; +} + +function getForbiddenGlobalTokens(platform = 'all') { + let tokenList = []; + platform = platform.toUpperCase(); + + Object.keys(globalVars).forEach(key => { + if (platform === 'ALL' || key !== platform) { + tokenList = tokenList.concat(globalVars[key]); + } + }); + + tokenList = uniq(tokenList); + + return tokenList; +} + + +function checkGlobal(path, tokenList) { + let messages = []; + + let programScope = path.scope; + + Object.keys(programScope.globals).forEach(tokenName => { + if (~tokenList.indexOf(tokenName)) { + messages.push({ + line: programScope.globals[tokenName].loc.start.line, + column: programScope.globals[tokenName].loc.start.column, + token: tokenName, + msg: 'global variable: "' + tokenName + '" should not be used in this file' + }); + } + }); + + return messages; +} /** * 校验语法 @@ -30,9 +97,10 @@ const handleProperty = function (property, propertyName, messages) { * @return {Object} 语法检查结果 */ const checkSyntax = function (part) { - const messages = []; + let messages = []; const opts = config.script; let ast; + let tokenList = []; try { ast = parse(part.content, opts); } @@ -44,33 +112,15 @@ const checkSyntax = function (part) { }); } try { + tokenList = getForbiddenGlobalTokens(part.platformType || 'all'); traverse(ast, { - enter(path) { - if (path.isClassProperty() && path.node) { - switch (path.node.key.name) { - case 'watch': - case 'computed': - case 'methods': - let properties = path.get('value').get('properties'); - if (properties.forEach) { - (properties || []).forEach(property => { - handleProperty(property, path.node.key.name, messages); - }); - } - break; - case 'beforeCreate': - case 'created': - case 'beforeMount': - case 'mounted': - case 'beforeDestroy': - case 'destroyed': - handleProperty(path, path.node.key.name, messages); - break; - default: - break; - } - } - + // check arrow function: we do not allow arrow functions in life cycle hooks. + ClassProperty(path) { + messages = messages.concat(checkArrowFun(path)); + }, + // check global variables: we shall never use any platform specified global variables such as wx, global, window etc. + Program(path) { + messages = messages.concat(checkGlobal(path, tokenList)); } }); } diff --git a/packages/chameleon-linter/package.json b/packages/chameleon-linter/package.json index b6e27eab9072917ffff904af02c83cbe848ba4da..7019be326ab37335d182a423524e9efee91bd794 100644 --- a/packages/chameleon-linter/package.json +++ b/packages/chameleon-linter/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "test": "nyc ./node_modules/mocha/bin/mocha ./test/**/*.test.js --timeout=3000", - "non-report-test": "./node_modules/mocha/bin/mocha ./test/template/linter/**/*.test.js", + "non-report-test": "./node_modules/mocha/bin/mocha ./test/cml.test.js", "break-non-report": "./node_modules/mocha/bin/mocha --inspect-brk ./test/template/linter/**/*.test.js" }, "bin": { diff --git a/packages/chameleon-linter/test/checker/cml/script/interfaces/prop-not-defined.interface b/packages/chameleon-linter/test/checker/cml/script/interfaces/prop-not-defined.interface new file mode 100644 index 0000000000000000000000000000000000000000..78e60f82b97d91b1dafea075b363b1d2ad287902 --- /dev/null +++ b/packages/chameleon-linter/test/checker/cml/script/interfaces/prop-not-defined.interface @@ -0,0 +1,46 @@ + + + + + + + + + + + diff --git a/packages/chameleon-linter/test/checker/cml/script/properties-methods/event-not-defined.interface b/packages/chameleon-linter/test/checker/cml/script/properties-methods/event-not-defined.interface new file mode 100644 index 0000000000000000000000000000000000000000..fb37b8b43aeb158b9b44e8e7977b6c29f60cf94a --- /dev/null +++ b/packages/chameleon-linter/test/checker/cml/script/properties-methods/event-not-defined.interface @@ -0,0 +1,28 @@ + diff --git a/packages/chameleon-linter/test/checker/cml/script/properties-methods/event-not-defined.web.cml b/packages/chameleon-linter/test/checker/cml/script/properties-methods/event-not-defined.web.cml new file mode 100644 index 0000000000000000000000000000000000000000..bf5349fb6719750238fabde64dad98d37e44773a --- /dev/null +++ b/packages/chameleon-linter/test/checker/cml/script/properties-methods/event-not-defined.web.cml @@ -0,0 +1,70 @@ + + + + diff --git a/packages/chameleon-linter/test/checker/cml/script/properties-methods/property-not-defined.interface b/packages/chameleon-linter/test/checker/cml/script/properties-methods/property-not-defined.interface new file mode 100644 index 0000000000000000000000000000000000000000..fb37b8b43aeb158b9b44e8e7977b6c29f60cf94a --- /dev/null +++ b/packages/chameleon-linter/test/checker/cml/script/properties-methods/property-not-defined.interface @@ -0,0 +1,28 @@ + diff --git a/packages/chameleon-linter/test/checker/cml/script/properties-methods/property-not-defined.wx.cml b/packages/chameleon-linter/test/checker/cml/script/properties-methods/property-not-defined.wx.cml new file mode 100644 index 0000000000000000000000000000000000000000..e614002ce7674b9e1f3c592b4d2f7b219bf8f206 --- /dev/null +++ b/packages/chameleon-linter/test/checker/cml/script/properties-methods/property-not-defined.wx.cml @@ -0,0 +1,64 @@ + + + + diff --git a/packages/chameleon-linter/test/checker/cml/script/standard.cml b/packages/chameleon-linter/test/checker/cml/script/standard.cml index cffbe76682e878392cae59c3dc7a95212e5f2764..717e29919f8d10b71029b364ff6c0a4e3bfced00 100644 --- a/packages/chameleon-linter/test/checker/cml/script/standard.cml +++ b/packages/chameleon-linter/test/checker/cml/script/standard.cml @@ -19,7 +19,6 @@ class ApplyRecord { pageData:{ type:Object, default(){ - window.location.href = 'xxx'; return {} } } diff --git a/packages/chameleon-linter/test/cml.test.js b/packages/chameleon-linter/test/cml.test.js index f0960ec08ce70ab324f00bc1208914d16867a925..707e814e06895ee570d07d0e0611d916da808a1c 100644 --- a/packages/chameleon-linter/test/cml.test.js +++ b/packages/chameleon-linter/test/cml.test.js @@ -7,12 +7,10 @@ const utils = require('../utils'); const fileSpec = require('../file-spec'); const chai = require('chai'); const assert = chai.assert; -const should = chai.should; const expect = chai.expect; const path = require('path'); - describe('cml', function() { @@ -128,9 +126,9 @@ describe('cml', function() { const result = await styleLinter(parts.style); expect(result.messages).to.deep.equal([ { - "column": 3, - "line": 7, - "msg": "expected \"indent\", got \";\"" + 'column': 3, + 'line': 7, + 'msg': 'expected "indent", got ";"' } ]); }); @@ -186,14 +184,14 @@ describe('cml', function() { expect(result.messages).to.deep.equal( [ { - "column": 5, - "line": 19, - "msg": "computed property \"hasApplyList\" cannot be used as an arrow function" + 'column': 5, + 'line': 19, + 'msg': 'computed property "hasApplyList" cannot be used as an arrow function' }, { - "column": 3, - "line": 30, - "msg": "lifecycle hook \"mounted\" cannot be used as an arrow function" + 'column': 3, + 'line': 30, + 'msg': 'lifecycle hook "mounted" cannot be used as an arrow function' } ] ); @@ -335,6 +333,10 @@ describe('cml', function() { }); }); describe('check-script', function () { + before(function() { + const projectRoot = path.resolve(__dirname, './checker/cml/script'); + config.init(projectRoot); + }); it('standard', async function () { const cmlPath = path.resolve(__dirname, './checker/cml/script/standard/standard.wx.cml'); const parts = utils.getCmlParts(cmlPath); @@ -343,13 +345,6 @@ describe('cml', function() { expect(result.script.messages).to.deep.equal([]); }); - it('no-interface', async function () { - const cmlPath = path.resolve(__dirname, './checker/cml/script/nointerface/nonstandard.wx.cml'); - const parts = utils.getCmlParts(cmlPath); - const result = await fileSpec.lintCmlFile(parts); - checkers.script(result); - assert.equal(result['interface'].messages.length, 1); - }); it('global-variable', async function () { const cmlPath = path.resolve(__dirname, './checker/cml/script/global-variable/standard.wx.cml'); const parts = utils.getCmlParts(cmlPath); @@ -371,5 +366,51 @@ describe('cml', function() { checkers.script(result); assert.equal(result.script.messages.length, 0); }); + it('property-not-defined', async function() { + const cmlPath = path.resolve(__dirname, './checker/cml/script/properties-methods/property-not-defined.wx.cml'); + const parts = utils.getCmlParts(cmlPath); + let result = await fileSpec.lintCmlFile(parts); + checkers.script(result); + assert.equal(result['interface'].messages.length, 1); + expect(result['interface'].messages[0]).to.include({ + line: 24, + column: 2, + token: 'text' + }); + }); + it('event-not-defined', async function() { + const cmlPath = path.resolve(__dirname, './checker/cml/script/properties-methods/event-not-defined.web.cml'); + const parts = utils.getCmlParts(cmlPath); + let result = await fileSpec.lintCmlFile(parts); + checkers.script(result); + assert.equal(result['interface'].messages.length, 1); + expect(result['interface'].messages[0]).to.include({ + line: 26, + column: 2, + token: 'top' + }); + assert.equal(result.script.messages.length, 2); + expect(result.script.messages).to.deep.equal([{ + line: 33, + column: 18, + token: 'create', + msg: 'event "create" is not defined in interface file "properties-methods/event-not-defined.interface"' + }, { + line: 37, + column: 18, + token: 'refOne', + msg: 'event "refOne" is not defined in interface file "properties-methods/event-not-defined.interface"' + }]); + }); + it('interface-prop-not-defined', async function() { + const cmlPath = path.resolve(__dirname, './checker/cml/script/interfaces/prop-not-defined.interface'); + const result = await fileSpec(cmlPath, 'interface'); + expect(result['interface'].messages).to.have.lengthOf(1); + expect(result['interface'].messages[0]).to.include({ + line: 3, + column: 2, + token: 'platform' + }); + }); }); }); diff --git a/packages/chameleon-linter/test/template/checker/template-lib-template.test.js b/packages/chameleon-linter/test/template/checker/template-lib-template.test.js index bba78ff28ce4fd6aa567307488812bde1ffbd231..0b08df231412999dc141743f22ed0f5dc5b6d08b 100644 --- a/packages/chameleon-linter/test/template/checker/template-lib-template.test.js +++ b/packages/chameleon-linter/test/template/checker/template-lib-template.test.js @@ -71,6 +71,26 @@ describe('template lib template', function() { method: false, variable: true, pos: [8, 46] + }, { + name: 'logicalLeft', + method: false, + variable: true, + pos: [9, 17] + }, { + name: 'logicalRight', + method: false, + variable: true, + pos: [9, 32] + }, { + name: 'eleOne', + method: false, + variable: true, + pos: [10, 18] + }, { + name: 'eleTwo', + method: false, + variable: true, + pos: [10, 26] }], 'variables mismatch'); expect(results.customizedComponents[0]['show-scroller']).to.have.deep.property('props', [{ event: false, diff --git a/packages/chameleon-linter/test/template/docs/check/success/index-lib-template-cml.cml b/packages/chameleon-linter/test/template/docs/check/success/index-lib-template-cml.cml index 53e1e5ede96100972816cd53335ee3704da4230d..2fd1d4209ebbb244fb902d1559a76c7c907dbf0c 100644 --- a/packages/chameleon-linter/test/template/docs/check/success/index-lib-template-cml.cml +++ b/packages/chameleon-linter/test/template/docs/check/success/index-lib-template-cml.cml @@ -6,6 +6,8 @@ + +