bifocal/node_modules/gscan/lib/ast-linter/rules/base.js

217 lines
8.4 KiB
JavaScript

const Scope = require('./internal/scope');
module.exports = class BaseRule {
constructor(options) {
this.ruleName = options.name;
this._log = options.log;
this.source = options.source;
this.partials = options.partials;
this.helpers = options.helpers;
this.inlinePartials = options.inlinePartials || [];
this.customThemeSettings = options.customThemeSettings;
// TODO: remove hardcoded list of known page builder properties once we have a way to get them from the spec
this.knownPageBuilderProperties = options.knownPageBuilderProperties || ['show_title_and_feature_image'];
}
getVisitor({fileName} = {}) {
const visitor = {};
const ruleVisitor = this.visitor();
// keep track of the locals that are available in the current context.
// enables checks in rules to see if a MustacheStatement is referring
// to a known local
const scope = (this.scope = new Scope());
// these are the node types that we care about
const nodeTypes = [
'Program',
'MustacheStatement',
'BlockStatement',
'PartialStatement',
'PartialBlockStatement',
'PathExpression',
'SubExpression',
'DecoratorBlock'
];
// this object keeps an array of functions for each node type, eventually
// it will be reduced to a single "visitor" where each nodetype is a
// function in order to match the Handlebars.Visitor prototype usage
const astVisitors = nodeTypes.reduce((obj, nodeType) => {
obj[nodeType] = [];
return obj;
}, {});
// Manage scope
// every time we hit a statement, use the parents array determine if
// any frames need to be removed from the context. This is necessary
// because Handlebars.Visitor does not have an `exit` event
nodeTypes.forEach((nodeType) => {
astVisitors[nodeType].push(function (node, hbVisitorInstance) {
// loop backwards through frames so that we're not popping
// frames that we're about to walk over
for (let i = scope.frames.length - 1; i >= 0; i--) {
let frame = scope.frames[i];
if ((frame.node !== node) && !hbVisitorInstance.parents.includes(frame.node)) {
scope.popFrame();
}
}
});
});
// for the very first, top-level Program node, push a template-level
// frame onto the stack so that we can determine the template context
astVisitors.Program.push(function (node) {
if (node.loc && node.loc.start.line === 1 && node.loc.column === 0) {
scope.pushTemplateFrame(node, {fileName});
}
});
// add the rule-specific visitors onto our visitors object. Do this
// before the BlockStatement context-building visitor so that
// BlockStatement rules are evaluated in the current context rather than
// their own context
for (const key in ruleVisitor) {
astVisitors[key].push(ruleVisitor[key]);
}
// push a frame onto the scope stack each time we enter into a block
// statement's body. Must happen after popping unknown frames so that we
// don't unintentionally pop the frame we've just pushed
astVisitors.BlockStatement.push(function (node) {
// these BlockStatements do not create a new context
const skipList = ['has', 'is', 'if', 'else', 'unless'];
const nodeName = node.path.parts[0];
if (!skipList.includes(nodeName)) {
scope.pushFrame(node);
}
});
// finally, build up the visitor object which will be attached to the
// Handlebars.Visitor prototype. Calls all handler functions that are in
// our astVisitors object for each node type
nodeTypes.forEach((nodeType) => {
visitor[nodeType] = (node, hbVisitorInstance) => {
// this.parents = hbVisitorInstance.parents;
astVisitors[nodeType].forEach(fn => fn(node, hbVisitorInstance));
};
});
return visitor;
}
// rules will extend this function
visitor() {}
log(result) {
const defaults = {
rule: this.ruleName
};
const reportedResult = Object.assign({}, defaults, result);
this._log(reportedResult);
}
isValidPartialReference(node, parents) {
return node.name.original === '@partial-block'
|| this.partials.includes(node.name.original)
|| this.isAccessibleInlinePartial(node, parents);
}
// Make sure one inline partial match the partial usage
isAccessibleInlinePartial(node, parents) {
const parentNodes = parents || [];
return this.inlinePartials.some((partial) => {
if (partial.node !== node.name.original) {
return false;
}
//filter candidates so that the inline partial is in the same scope as where it's used
for (let i = 0; i < partial.parents.length; i++) {
const declarationParent = partial.parents[i];
for (let j = 0; j < parentNodes.length; j++) {
const usageParent = parentNodes[j];
// If we found a common ancestor, we're good
// To compare two nodes, we check the type of the node and the location of the code
if (usageParent.type === declarationParent.type &&
usageParent.loc.source === declarationParent.loc.source &&
usageParent.loc.start.line === declarationParent.loc.start.line &&
usageParent.loc.start.column === declarationParent.loc.start.column &&
usageParent.loc.end.line === declarationParent.loc.end.line &&
usageParent.loc.end.column === declarationParent.loc.end.column) {
return true;
}
// If we found a block before finding a common ancestor, the usage can't access the declaration
if (['BlockStatement','PartialBlockStatement','DecoratorBlock'].includes(declarationParent.type)) {
return false;
}
}
}
// The default is that the usage is in scope of the declaration
return true;
});
}
isValidHelperReference(nodeName) {
return this.helpers && this.helpers.includes(nodeName);
}
isValidPageBuilderProperty(property) {
return this.knownPageBuilderProperties && this.knownPageBuilderProperties.includes(property);
}
isValidCustomThemeSettingReference(name) {
return this.customThemeSettings && !!this.customThemeSettings[name];
}
isSelectCustomTheme(name) {
return this.customThemeSettings && this.customThemeSettings[name] && this.customThemeSettings[name].type === 'select';
}
isValidCustomThemeSettingSelectValue(name, value) {
return this.isSelectCustomTheme(name) && this.customThemeSettings[name].options.includes(value);
}
// mostly copy/pasta from tildeio/htmlbars with a few tweaks:
// https://github.com/tildeio/htmlbars/blob/v0.14.17/packages/htmlbars-syntax/lib/parser.js#L59-L90
sourceForNode(node) {
if (!node.loc) {
return;
}
let source = this.source.split('\n');
let firstLine = node.loc.start.line - 1;
let lastLine = node.loc.end.line - 1;
let currentLine = firstLine - 1;
let firstColumn = node.loc.start.column;
let lastColumn = node.loc.end.column;
let string = [];
let line;
while (currentLine < lastLine) {
currentLine += 1;
line = source[currentLine];
if (currentLine === firstLine) {
if (firstLine === lastLine) {
string.push(line.slice(firstColumn, lastColumn));
} else {
string.push(line.slice(firstColumn));
}
} else if (currentLine === lastLine) {
string.push(line.slice(0, lastColumn));
} else {
string.push(line);
}
}
return string.join('');
}
};