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
JavaScript
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;
},
},
};
}
}