UNPKG

ember-template-lint

Version:
425 lines (393 loc) 12.3 kB
'use strict'; const Fuse = require('fuse.js'); const Rule = require('./_base'); function deprecateArgument(componentName, argumentName, replacementAttribute) { const msgs = [`Passing the "${argumentName}" argument to <${componentName} /> is deprecated.`]; if (replacementAttribute) { msgs.push( `Instead, please pass the attribute directly, i.e. "<${componentName} ${replacementAttribute}={{...}} />" instead of "<${componentName} ${argumentName}={{...}} />".` ); } return msgs.join('\n'); } function deprecateEvent(componentName, argumentName, replacementAttribute) { const msgs = [`Passing the "${argumentName}" argument to <${componentName} /> is deprecated.`]; if (replacementAttribute) { msgs.push( `Instead, please use the {{on}} modifier, i.e. "<${componentName} {{on "${replacementAttribute}" ...}} />" instead of "<${componentName} ${argumentName}={{...}} />".` ); } return msgs.join('\n'); } // from https://github.com/emberjs/rfcs/blob/master/text/0707-modernize-built-in-components-2.md#summary const KnownArguments = { LinkTo: { arguments: [ 'route', 'model', 'models', 'query', 'replace', 'disabled', 'current-when', 'activeClass', 'loadingClass', 'disabledClass', ], deprecatedArguments: { // key -> replacement attr '@active': '', '@loading': '', '@init': '', '@didRender': '', '@willDestroy': '', '@didReceiveAttrs': '', '@willRender': '', '@didInsertElement': '', '@didUpdateAttrs': '', '@willUpdate': '', '@didUpdate': '', '@willDestroyElement': '', '@willClearRender': '', '@didDestroyElement': '', '@tagName': '', '@id': 'id', '@elementId': 'id', '@ariaRole': 'role', '@class': 'class', '@classNames': 'class', '@classNameBindings': 'class', '@isVisible': 'style', '@rel': 'rel', '@tabindex': 'tabindex', '@target': 'target', '@title': 'title', }, deprecatedEvents: { '@click': 'click', '@contextMenu': 'contextmenu', '@doubleClick': 'dblclick', '@drag': 'drag', '@dragEnd': 'dragend', '@dragEnter': 'dragenter', '@dragLeave': 'dragleave', '@dragOver': 'dragover', '@dragStart': 'dragstart', '@drop': 'drop', '@focusIn': 'focusin', '@focusOut': 'focusout', '@input': 'input', '@keyDown': 'keydown', '@keyPress': 'keypress', '@keyUp': 'keyup', '@mouseDown': 'mousedown', '@mouseEnter': 'mouseenter', '@mouseLeave': 'mouseleave', '@mouseMove': 'mousemove', '@mouseUp': 'mouseup', '@submit': 'submit', '@touchCancel': 'touchcancel', '@touchEnd': 'touchend', '@touchMove': 'touchmove', '@touchStart': 'touchstart', }, conflicts: [['model', 'models']], required: [['route', 'query']], }, Input: { arguments: ['type', 'value', 'checked', 'insert-newline', 'enter', 'escape-press'], deprecatedArguments: { '@bubbles': '', '@cancel': '', '@init': '', '@didRender': '', '@willDestroy': '', '@didReceiveAttrs': '', '@willRender': '', '@didInsertElement': '', '@didUpdateAttrs': '', '@willUpdate': '', '@didUpdate': '', '@willDestroyElement': '', '@willClearRender': '', '@didDestroyElement': '', '@id': 'id', '@elementId': 'id', '@ariaRole': 'role', '@class': 'class', '@classNames': 'class', '@classNameBindings': 'class', '@isVisible': 'style', '@accept': 'accept', '@autocapitalize': '', '@autocomplete': 'autocomplete', '@autocorrect': '', '@autofocus': 'autofocus', '@autosave': '', '@dir': 'dir', '@disabled': 'disabled', '@form': 'form', '@formaction': 'formaction', '@formenctype': 'formenctype', '@formmethod': 'formmethod', '@formnovalidate': 'formnovalidate', '@formtarget': 'formtarget', '@height': 'height', '@indeterminate': '', '@inputmode': '', '@lang': 'lang', '@list': 'list', '@max': 'max', '@maxlength': 'maxlength', '@min': 'min', '@minlength': 'minlength', '@multiple': 'multiple', '@name': 'name', '@pattern': 'pattern', '@placeholder': 'placeholder', '@readonly': 'readonly', '@required': 'required', '@selectionDirection': '', '@size': 'size', '@spellcheck': 'spellcheck', '@step': 'step', '@tabindex': 'tabindex', '@title': 'title', '@width': 'width', }, conflicts: [['checked', 'value']], deprecatedEvents: { '@change': 'change', '@click': 'click', '@contextMenu': 'contextmenu', '@doubleClick': 'dblclick', '@drag': 'drag', '@dragEnd': 'dragend', '@dragEnter': 'dragenter', '@dragLeave': 'dragleave', '@dragOver': 'dragover', '@dragStart': 'dragstart', '@drop': 'drop', '@input': 'input', '@mouseDown': 'mousedown', '@mouseEnter': 'mouseenter', '@mouseLeave': 'mouseleave', '@mouseMove': 'mousemove', '@mouseUp': 'mouseup', '@submit': 'submit', '@touchCancel': 'touchcancel', '@touchEnd': 'touchend', '@touchMove': 'touchmove', '@touchStart': 'touchstart', '@focus-in': 'focusin', '@focus-out': 'focusout', '@key-down': 'keydown', '@key-press': 'keypress', '@key-up': 'keyup', }, }, Textarea: { arguments: ['value', 'insert-newline', 'enter', 'escape-press'], deprecatedArguments: { '@init': '', '@didRender': '', '@willDestroy': '', '@didReceiveAttrs': '', '@willRender': '', '@didInsertElement': '', '@didUpdateAttrs': '', '@willUpdate': '', '@didUpdate': '', '@willDestroyElement': '', '@willClearRender': '', '@didDestroyElement': '', '@id': 'id', '@elementId': 'id', '@ariaRole': 'role', '@class': 'class', '@classNames': 'class', '@classNameBindings': 'class', '@isVisible': 'style', '@autocapitalize': '', '@autocomplete': 'autocomplete', '@autocorrect': '', '@autofocus': 'autofocus', '@cols': 'cols', '@dir': 'dir', '@disabled': 'disabled', '@form': 'form', '@lang': 'lang', '@maxlength': 'maxlength', '@minlength': 'minlength', '@name': 'name', '@placeholder': 'placeholder', '@readonly': 'readonly', '@required': 'required', '@rows': 'rows', '@selectionDirection': '', '@selectionEnd': '', '@selectionStart': '', '@spellcheck': 'spellcheck', '@tabindex': 'tabindex', '@title': 'title', '@wrap': 'wrap', }, deprecatedEvents: { '@bubbles': '', '@cancel': '', '@click': 'click', '@contextMenu': 'contextmenu', '@doubleClick': 'dblclick', '@drag': 'drag', '@dragEnd': 'dragend', '@dragEnter': 'dragenter', '@dragLeave': 'dragleave', '@dragOver': 'dragover', '@dragStart': 'dragstart', '@drop': 'drop', '@input': 'input', '@mouseDown': 'mousedown', '@mouseEnter': 'mouseenter', '@mouseLeave': 'mouseleave', '@mouseMove': 'mousemove', '@mouseUp': 'mouseup', '@submit': 'submit', '@touchCancel': 'touchcancel', '@touchEnd': 'touchend', '@touchMove': 'touchmove', '@touchStart': 'touchstart', '@focus-in': 'focusin', '@focus-out': 'focusout', '@key-down': 'keydown', '@key-press': 'keypress', '@key-up': 'keyup', }, }, }; function removeAtSymbol(txt) { return txt.replace('@', ''); } function ERROR_MESSAGE(tagName, argumentName) { const tagMeta = KnownArguments[tagName]; const deprecatedArgs = tagMeta.deprecatedArguments || {}; const deprecatedEvents = tagMeta.deprecatedEvents || {}; const candidates = [ ...new Set([ ...tagMeta.arguments, ...Object.keys(deprecatedArgs).map((e) => removeAtSymbol(e)), ...Object.keys(deprecatedEvents).map((e) => removeAtSymbol(e)), ]), ]; const pureQuery = removeAtSymbol(argumentName); let query = pureQuery; let fuzzyResults = []; while (!fuzzyResults.length && query.length) { fuzzyResults = new Fuse(candidates).search(query); query = query.slice(0, -1); } let closestKey = fuzzyResults.length ? `@${fuzzyResults[0].item}` : argumentName; if (closestKey in deprecatedArgs) { return deprecateArgument(tagName, closestKey, deprecatedArgs[closestKey]); } if (closestKey in deprecatedEvents) { return deprecateEvent(tagName, closestKey, deprecatedEvents[closestKey]); } const msg = `"${argumentName}" is unknown argument for <${tagName} /> component.`; if (fuzzyResults.length) { return `${msg} Did you mean "@${fuzzyResults[0].item}"?`; } else { return msg; } } function REQUIRED_MESSAGE(tagName, argumentNames) { return `Argument${argumentNames.length > 1 ? 's' : ''} ${argumentNames .map((el) => `"@${el}"`) .join(' or ')} is required for <${tagName} /> component.`; } function CONFLICT_MESSAGE(argumentName, rawList) { const conflictsList = rawList.filter((el) => `@${el}` !== argumentName); return `"${argumentName}" conflicts with ${conflictsList .map((el) => `"@${el}"`) .join(', ')}, only one should exists.`; } function isArgument(attributeNode) { return attributeNode.name.startsWith('@'); } function pureName(attributeNode) { return removeAtSymbol(attributeNode.name); } module.exports = class NoUnknownArgumentsForBuiltinComponents extends Rule { visitor() { return { ElementNode(node) { let nodeMeta = KnownArguments[node.tag]; if (!nodeMeta) { return; } let warns = []; let seen = []; const logError = (attr) => { this.log({ message: ERROR_MESSAGE(node.tag, attr.name), line: attr.loc && attr.loc.start.line, column: attr.loc && attr.loc.start.column, source: (this.sourceForNode(attr) || '').split('=')[0], }); }; const logConflict = (attr, conflictList) => { this.log({ message: CONFLICT_MESSAGE(attr.name, conflictList), line: attr.loc && attr.loc.start.line, column: attr.loc && attr.loc.start.column, source: (this.sourceForNode(attr) || '').split('=')[0], }); }; const logRequired = (variants) => { this.log({ message: REQUIRED_MESSAGE(node.tag, variants), line: node.loc && node.loc.start.line, column: node.loc && node.loc.start.column + 1, source: node.tag, }); }; for (let argument of node.attributes) { if (!isArgument(argument)) { continue; } const argumentName = pureName(argument); if (!nodeMeta.arguments.includes(argumentName)) { warns.push(argument); } else { seen.push(argumentName); } } for (let warn of warns) { logError(warn); } if ('conflicts' in nodeMeta) { for (let conflictList of nodeMeta.conflicts) { if (conflictList.every((item) => seen.includes(item))) { for (let argumentName of conflictList) { const attr = node.attributes.find(({ name }) => `@${argumentName}` === name); if (attr) { logConflict(attr, conflictList); } } } } } if ('required' in nodeMeta) { for (let requiredItems of nodeMeta.required) { let variants = Array.isArray(requiredItems) ? requiredItems : [requiredItems]; if (!variants.some((el) => seen.includes(el))) { logRequired(variants); } } } }, }; } }; module.exports.ERROR_MESSAGE = ERROR_MESSAGE; module.exports.CONFLICT_MESSAGE = CONFLICT_MESSAGE; module.exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;