UNPKG

ember-template-lint-no-implicit-this

Version:

A no 'implicit this rule' but with fixing plugin for ember-template-lint

167 lines (145 loc) 4.29 kB
import { Rule } from "ember-template-lint"; function createErrorMessage(ruleName, lines, config) { return [ `The ${ruleName} rule accepts one of the following values.`, lines, `You specified \`${JSON.stringify(config)}\``, ].join("\n"); } function message(original) { return ( `Ambiguous path '${original}' is not allowed. ` + `Use '@${original}' if it is a named argument ` + `or 'this.${original}' if it is a property on 'this'. ` + "If it is a helper or component that has no arguments, " + "you must either convert it to an angle bracket invocation " + "or manually add it to the 'no-implicit-this-fix' rule configuration, e.g. " + `'no-implicit-this-fix': { allow: ['${original}'] }.` ); } function isString(value) { return typeof value === "string"; } function isRegExp(value) { return value instanceof RegExp; } function allowedFormat(value) { return isString(value) || isRegExp(value); } // Allow Ember's builtin argless syntaxes export const ARGLESS_BUILTIN_HELPERS = [ "array", "concat", "debugger", "has-block", "hasBlock", "has-block-params", "hasBlockParams", "hash", "input", "log", "outlet", "query-params", "textarea", "yield", "unique-id", ]; // arg'less Components / Helpers in default ember-cli blueprint const ARGLESS_DEFAULT_BLUEPRINT = [ "welcome-page", /* from app/index.html and tests/index.html */ "rootURL", ]; export default class NoImplicitThisFix extends Rule { parseConfig(config) { if (this.isStrictMode === false) { return false; } switch (typeof config) { case "undefined": return false; case "boolean": if (config) { return { allow: [...ARGLESS_BUILTIN_HELPERS, ...ARGLESS_DEFAULT_BLUEPRINT], }; } else { return false; } case "object": if (Array.isArray(config.allow) && config.allow.every(allowedFormat)) { return { allow: [ ...ARGLESS_BUILTIN_HELPERS, ...ARGLESS_DEFAULT_BLUEPRINT, ...config.allow, ], }; } break; } let errorMessage = createErrorMessage( this.ruleName, [ " * boolean - `true` to enable / `false` to disable", " * object -- An object with the following keys:", " * `allow` -- An array of component / helper names for that may be called without arguments", ], config ); throw new Error(errorMessage); } // The way this visitor works is a bit sketchy. We need to lint the PathExpressions // in the callee position differently those in an argument position. // // Unfortunately, the current visitor API doesn't give us a good way to differentiate // these two cases. Instead, we rely on the fact that the _first_ PathExpression that // we enter after entering a MustacheStatement/BlockStatement/... will be the callee // and we track this using a flag called `nextPathIsCallee`. visitor() { let nextPathIsCallee = false; return { PathExpression(node) { if (nextPathIsCallee) { // All paths are valid callees so there's nothing to check. } else { let valid = node.data || node.this || this.isLocal(node) || this.config.allow.some((item) => { return isRegExp(item) ? item.test(node.original) : item === node.original; }); if (!valid) { if (this.mode === "fix") { node.original = `this.${node.original}`; } else { this.log({ message: message(node.original), node, isFixable: true, }); } } } nextPathIsCallee = false; }, SubExpression() { nextPathIsCallee = true; }, ElementModifierStatement() { nextPathIsCallee = true; }, MustacheStatement(node) { nextPathIsCallee = node.params.length > 0 || node.hash.pairs.length > 0; }, BlockStatement: { enter() { nextPathIsCallee = true; }, }, }; } }