190 lines
6.2 KiB
JavaScript
190 lines
6.2 KiB
JavaScript
const Handlebars = require('handlebars');
|
|
const {normalizePath} = require('../utils');
|
|
const defaultRules = require('./rules');
|
|
|
|
class Linter {
|
|
/**
|
|
*
|
|
* @param {Object} [options]
|
|
* @param {string[]?} options.partials - list of known theme partial names in ['mypartial', 'logo'] format
|
|
* @param {string[]?} options.helpers - list of registered theme helper names in ['is', 'has'] format
|
|
* @param {string[]?} options.inlinePartials - list of known local partial names in ['mypartial', 'logo'] format
|
|
* @param {Object?} options.customThemeSettings - list of registered custom theme settings in {'main_accent':{}} format
|
|
*/
|
|
constructor(options = {partials: [], helpers: [], inlinePartials: [], customThemeSettings: {}}) {
|
|
this.options = options;
|
|
// Ignore OS-specific path separator as Handlebars only uses forward-separators in its syntax
|
|
this.options.partials = this.options.partials.map(partial => normalizePath(partial));
|
|
|
|
this.partials = [];
|
|
this.helpers = [];
|
|
this.inlinePartials = [];
|
|
this.customThemeSettings = [];
|
|
this.usedPageProperties = [];
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} config
|
|
* @param {Object[]} config.rules - instances of AST Rule classes
|
|
* @param {Function} config.log - rule check reporting log function
|
|
* @param {string} config.source - content on the Handlebars template to be checked
|
|
*
|
|
* @returns {Scanner} instance of a Scanner class
|
|
*/
|
|
buildScanner(config) {
|
|
const nodeHandlers = [];
|
|
|
|
for (const ruleName in config.rules) {
|
|
let Rule = config.rules[ruleName];
|
|
let rule = new Rule({
|
|
name: ruleName,
|
|
log: config.log,
|
|
source: config.source,
|
|
partials: this.options.partials,
|
|
helpers: this.options.helpers,
|
|
inlinePartials: this.options.inlinePartials,
|
|
customThemeSettings: this.options.customThemeSettings
|
|
});
|
|
|
|
nodeHandlers.push({
|
|
rule,
|
|
visitor: rule.getVisitor()
|
|
});
|
|
}
|
|
|
|
function Scanner() {
|
|
this.context = {
|
|
partials: [],
|
|
helpers: [],
|
|
inlinePartials: [],
|
|
customThemeSettings: [],
|
|
usedPageProperties: []
|
|
};
|
|
}
|
|
Scanner.prototype = new Handlebars.Visitor();
|
|
|
|
const nodeTypes = [
|
|
'Program',
|
|
'MustacheStatement',
|
|
'BlockStatement',
|
|
'PartialStatement',
|
|
'PartialBlockStatement',
|
|
'PathExpression',
|
|
'SubExpression',
|
|
'DecoratorBlock'
|
|
// the following types are not used in Ghost or we don't validate
|
|
// 'ContentStatement',
|
|
// 'CommentStatement,
|
|
// 'Decorator',
|
|
];
|
|
|
|
nodeTypes.forEach((nodeType) => {
|
|
Scanner.prototype[nodeType] = function (node) {
|
|
nodeHandlers.forEach((handler) => {
|
|
if (handler.visitor[nodeType]) {
|
|
handler.visitor[nodeType].call(handler.rule, node, this);
|
|
}
|
|
});
|
|
|
|
Handlebars.Visitor.prototype[nodeType].call(this, node);
|
|
};
|
|
});
|
|
|
|
const scanner = new Scanner();
|
|
|
|
nodeHandlers.forEach((handler) => {
|
|
handler.rule.scanner = scanner;
|
|
});
|
|
|
|
return scanner;
|
|
}
|
|
|
|
/**
|
|
* The main function for the Linter class. It takes the source code to lint
|
|
* and returns the results.
|
|
*
|
|
* @param {Object} options
|
|
* @param {string} options.source - The source code to verify.
|
|
* @param {Object} options.parsed - The parsing results.
|
|
* @param {Object} options.parsed.ast - The ast tree to lint.
|
|
* @param {Object} options.parsed.error - An error that happened when parsing.
|
|
* @param {string} options.moduleId - Name of the source code to identify by.
|
|
* @param {BaseRule[]} options.rules - Array of Rule class instances to use for verification.
|
|
*
|
|
* @returns {LintResult[]} messages - lint results.
|
|
*/
|
|
verify(options) {
|
|
const messages = [];
|
|
|
|
function addToMessages(_message) {
|
|
let message = Object.assign({}, {moduleId: options.moduleId}, _message);
|
|
messages.push(message);
|
|
}
|
|
|
|
const scannerConfig = {
|
|
rules: options.rules || defaultRules,
|
|
log: addToMessages,
|
|
source: options.source
|
|
};
|
|
|
|
const scanner = this.buildScanner(scannerConfig);
|
|
|
|
if (options.parsed.error) {
|
|
const err = options.parsed.error;
|
|
addToMessages({
|
|
message: err.message,
|
|
fatal: true,
|
|
column: err.column,
|
|
line: err.lineNumber
|
|
});
|
|
|
|
return messages;
|
|
}
|
|
|
|
scanner.accept(options.parsed.ast);
|
|
|
|
if (scanner.context.partials) {
|
|
this.partials = scanner.context.partials;
|
|
}
|
|
|
|
if (scanner.context.helpers) {
|
|
this.helpers = scanner.context.helpers.map(p => ({
|
|
name: p.node,
|
|
helperType: p.helperType
|
|
}));
|
|
}
|
|
|
|
if (scanner.context.customThemeSettings) {
|
|
this.customThemeSettings = scanner.context.customThemeSettings;
|
|
}
|
|
|
|
if (scanner.context.inlinePartials) {
|
|
this.inlinePartials = scanner.context.inlinePartials;
|
|
}
|
|
|
|
if (scanner.context.usedPageProperties) {
|
|
this.usedPageProperties = scanner.context.usedPageProperties;
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
}
|
|
|
|
module.exports = Linter;
|
|
|
|
/**
|
|
* @typedef {Object} LintResult
|
|
* @prop {string} rule - The name of the rule that triggered this warning/error.
|
|
* @prop {string} message - The message that should be output.
|
|
* @prop {number} line - The line on which the error occurred.
|
|
* @prop {number} column - The column on which the error occurred.
|
|
* @prop {string} moduleId - The module path for the file containing the error.
|
|
* @prop {string} source - The source that caused the error.
|
|
* @prop {string} fix - An object describing how to fix the error.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./rules/base')} BaseRule
|
|
*/
|