ember-template-lint
Version:
Linter for Ember or Handlebars templates.
217 lines (186 loc) • 6.5 kB
JavaScript
import { builders as b } from 'ember-template-recast';
import AstNodeInfo from '../helpers/ast-node-info.js';
import createErrorMessage from '../helpers/create-error-message.js';
import Rule from './_base.js';
const messages = {
followingElseBlock: 'Using an {{else}} block with {{unless}} should be avoided.',
asElseUnlessBlock: 'Using an `{{else unless}}` block should be avoided.',
withHelper: 'Using {{unless}} in combination with other helpers should be avoided.',
};
const DEFAULT_CONFIG = {
allowlist: [],
denylist: [],
maxHelpers: 1,
};
function isValidConfigObjectFormat(config) {
for (let key in DEFAULT_CONFIG) {
let value = config[key];
let valueIsArray = Array.isArray(value);
if (value === undefined) {
config[key] = DEFAULT_CONFIG[key];
} else if (['allowlist', 'denylist'].includes(key) && !valueIsArray) {
return false;
}
}
return true;
}
export default class SimpleUnless extends Rule {
parseConfig(config) {
let configType = typeof config;
switch (configType) {
case 'boolean': {
// if `true` use `DEFAULT_CONFIG`
return config ? DEFAULT_CONFIG : false;
}
case 'object': {
if (isValidConfigObjectFormat(config)) {
return config;
}
break;
}
case 'undefined': {
return false;
}
}
let errorMessage = createErrorMessage(
this.ruleName,
[
' * boolean -- `true` for enabled / `false` for disabled\n' +
' * object --\n' +
" * `allowlist` -- array - `['or']` for specific helpers / `[]` for wildcard\n" +
" * `denylist` -- array - `['or']` for specific helpers / `[]` for none\n" +
' * `maxHelpers` -- number - default `1` - use `-1` for no limit',
],
config
);
throw new Error(errorMessage);
}
/**
* @returns {import('./types.js').VisitorReturnType<SimpleUnless>}
*/
visitor() {
return {
MustacheStatement(node) {
const fixMode = this.mode === 'fix';
if (node.path.original === 'unless' && node.params[0].path) {
this._withHelper(node, fixMode);
}
},
BlockStatement(node) {
const nodeInverse = node.inverse;
const fixMode = this.mode === 'fix';
if (nodeInverse) {
if (AstNodeInfo.isUnless(node)) {
if (nodeInverse.body[0] && AstNodeInfo.isIf(nodeInverse.body[0])) {
this._followingElseIfBlock(node);
} else {
this._followingElseBlock(node, fixMode);
}
} else if (this._isElseUnlessBlock(nodeInverse.body[0])) {
this._asElseUnlessBlock(node);
}
} else if (AstNodeInfo.isUnless(node) && node.params[0].path) {
this._withHelper(node, fixMode);
}
},
};
}
_followingElseBlock(block, fixMode) {
if (fixMode) {
const programBody = block.program.body;
block.program.body = block.inverse.body;
block.inverse.body = programBody;
block.path.original = 'if';
return;
}
let node = block.program;
let actual = '{{else}}';
this._logMessage(node, messages.followingElseBlock, actual, true);
}
_followingElseIfBlock(block) {
let inverse = block.inverse;
let node = block.program;
let parameter = inverse.body[0].params[0].original;
let actual = `{{else if ${parameter}}}`;
this._logMessage(node, messages.followingElseBlock, actual);
}
_asElseUnlessBlock(block) {
let inverse = block.inverse;
let node = inverse.body[0];
let actual = '{{else unless ...';
this._logMessage(node, messages.asElseUnlessBlock, actual);
}
_withHelper(node, fixMode) {
let { allowlist = [], denylist = [], maxHelpers } = this.config;
let params;
let nextParams = node.params;
let helperCount = 0;
let containsSubExpression = false;
let shouldFix = false;
do {
params = nextParams;
nextParams = [];
for (const param of params) {
if (param.type === 'SubExpression') {
if (++helperCount > maxHelpers && maxHelpers > -1) {
let actual = `{{unless ${helperCount > 1 ? '(... ' : ''}(${param.path.original} ...`;
let message = `${messages.withHelper} MaxHelpers: ${maxHelpers}`;
this._logMessage(param, message, actual, true);
shouldFix = true;
}
if (allowlist.length > 0 && !allowlist.includes(param.path.original)) {
let actual = `{{unless ${helperCount > 1 ? '(... ' : ''}(${param.path.original} ...`;
let message = `${messages.withHelper} Allowed helper${
allowlist.length > 1 ? 's' : ''
}: ${allowlist.toString()}`;
this._logMessage(param, message, actual, true);
shouldFix = true;
}
if (denylist.length > 0 && denylist.includes(param.path.original)) {
let actual = `{{unless ${helperCount > 1 ? '(... ' : ''}(${param.path.original} ...`;
let message = `${messages.withHelper} Restricted helper${
denylist.length > 1 ? 's' : ''
}: ${denylist.toString()}`;
this._logMessage(param, message, actual, true);
shouldFix = true;
}
for (const p of param.params) {
nextParams.push(p); // nextParams.push(...param.params);
}
}
}
containsSubExpression = nextParams.some((param) => param.type === 'SubExpression');
} while (containsSubExpression);
if (fixMode && shouldFix) {
node.path.original = 'if';
if (node.params[0].type === 'SubExpression' && node.params[0].path.original === 'not') {
// skip creating subexpression so we won't create `(not (not` chain
if (node.params[0].params.length > 1) {
node.params[0].path.original = 'or';
return;
}
const [nothelper, ...rest] = [...node.params];
node.params = nothelper.params;
node.params.push(...rest);
return;
}
node.params[0] = b.sexpr('not', [node.params[0]]);
}
}
_isElseUnlessBlock(node) {
return (
node &&
node.path &&
node.path.original === 'unless' &&
this.sourceForNode(node).startsWith('{{else ')
);
}
_logMessage(node, message, source, isFixable = false) {
return this.log({
message,
node,
source,
isFixable,
});
}
}