UNPKG

@yusufkandemir/eslint-plugin-lodash-template

Version:

ESLint plugin for John Resig-style micro template, Lodash's template, Underscore's template and EJS.

292 lines (259 loc) 8.67 kB
// @ts-check "use strict"; const path = require("path"); const hash = require("../../utils/hash"); const sharedContainer = require("../../shared-container"); const microTemplateProcessor = require("../micro-template-processor"); const extractAllTemplates = require("./extract-all-templates"); const { groupingMessages, filterDuplicateMessages, } = require("../utils/messages"); const baseMeta = require("../../meta"); /** * @typedef {import('../../services/micro-template-service')} MicroTemplateService */ /** * Check if A equal B (A == B) * @param {object} a The location. * @param {object} b The location. */ function eq(a, b) { return a.line === b.line && a.column === b.column; } /** * Check if A less than B (A < B) * @param {object} a The location. * @param {object} b The location. */ function lt(a, b) { if (a.line < b.line) { return true; } if (a.line > b.line) { return false; } return a.column < b.column; } /** * Normalize location data */ function normalizeMessageLocs(message) { const messageLoc = { start: { line: message.line, column: message.column - 1, }, }; if (message.endLine != null && message.endColumn != null) { messageLoc.end = { line: message.endLine, column: message.endColumn - 1, }; } return messageLoc; } /** * Filter tag messages. * @param {Array<*>}messages The base messages. * @param {MicroTemplateService} microTemplateService The MicroTemplateService. * @returns {Array<*>} messages The filtered messages. */ function filterTagMessages(messages, microTemplateService) { return messages.filter((message) => { const messageLoc = normalizeMessageLocs(message); if ( intersectsTemplateTag( (token) => intersectsMessageLocs(messageLoc, token), microTemplateService, ) ) { return false; } return true; }); } /** * Check whether the location is in range location. * @param {object} loc The location. * @param {object} start The start location. * @param {object} end The end location. * @returns {boolean} `true` if the location is in range location. */ function locationInRangeLoc(loc, start, end) { if (loc.line < start.line || end.line < loc.line) { return false; } if (loc.line === start.line) { if (start.column > loc.column) { return false; } } if (loc.line === end.line) { if (loc.column >= end.column) { return false; } } return true; } /** * Filter striped location messages. * @param {Array<*>} messages The base messages. * @param {[number, number][]} stripedRanges The striped ranges. * @param {MicroTemplateService} microTemplateService The MicroTemplateService. * @returns {Array<*>} messages The filtered messages. */ function filterStripedMessages(messages, stripedRanges, microTemplateService) { const stripedLocs = stripedRanges.map((range) => ({ start: microTemplateService.getLocFromIndex(range[0]), end: microTemplateService.getLocFromIndex(range[1]), })); return messages.filter( (message) => !stripedLocs.some((stripedLoc) => locationInRangeLoc(message, stripedLoc.start, stripedLoc.end), ), ); } /** * Disable autofix that intersect template tags. * @param {Array} messages The base messages. * @param {MicroTemplateService} microTemplateService The MicroTemplateService. * @returns {Array} messages The processed messages. */ function disableTagsAutofix(messages, microTemplateService) { return messages.map((message) => { if (message.fix) { const range = message.fix.range; if ( intersectsTemplateTag((token) => { const a = range; const b = token.range; return a[0] < b[1] && b[0] < a[1]; }, microTemplateService) ) { const obj = Object.assign({}, message); delete obj.fix; return obj; } } return message; }); } /** * Check whether the location is intersects template tag. * @param {function} checkIntersects The intersects check function. * @param {MicroTemplateService} microTemplateService The MicroTemplateService. * @returns {boolean} `true` if the location is intersects template tag. */ function intersectsTemplateTag(checkIntersects, microTemplateService) { for (const token of microTemplateService.getMicroTemplateTokens()) { if (checkIntersects(token)) { return true; } } return false; } /** * Check if the location ranges intersect. * @param {object} messageLoc The message locations. * @param {object} token The token. */ function intersectsMessageLocs(messageLoc, token) { const uselessMessageLocEnd = messageLoc.end == null || eq(messageLoc.start, messageLoc.end); if (uselessMessageLocEnd) { return ( lt(token.loc.start, /* < */ messageLoc.start) && lt(messageLoc.start, /* < */ token.loc.end) ); } return ( lt(messageLoc.start, /* < */ token.loc.end) && lt(token.loc.start, /* < */ messageLoc.end) ); } let processedStore = null; module.exports = { preprocess(code, filename) { if (sharedContainer.getPathCoveredTemplate(filename)) { // already processed return [code]; } const { templates, microTemplateService, error } = extractAllTemplates( code, filename, ); if (error) { return microTemplateProcessor.preprocess(code, filename); } if (!microTemplateService.getMicroTemplateTokens().length) { // not template return [code]; } const container = sharedContainer.register(filename); const origExt = path.extname(filename); const codes = templates.map((pathCoveredTemplate, index) => { const prefix = hash(pathCoveredTemplate.template); const scriptFilename = `@yusufkandemir/eslint-plugin-lodash-template-${prefix}${index}${ origExt || ".js" }`; container.addPathCoveredTemplate( scriptFilename, pathCoveredTemplate, ); return { text: code, filename: scriptFilename, }; }); processedStore = { templates }; return microTemplateProcessor.preprocess(code, filename).concat(codes); }, postprocess(messages, filename) { const templates = processedStore && processedStore.templates; processedStore = null; const container = sharedContainer.get(filename); const microTemplateService = container && container.getService(); let scriptMessages = messages.slice(1); if (microTemplateService) { scriptMessages = scriptMessages.map((ms) => filterTagMessages(ms, microTemplateService), ); if (templates) { scriptMessages = scriptMessages.map((ms, index) => filterStripedMessages( ms, templates[index].stripedRanges, microTemplateService, ), ); } scriptMessages = scriptMessages.map((ms) => disableTagsAutofix(ms, microTemplateService), ); const adjustScriptMessages = []; for (const msgs of groupingMessages(scriptMessages).values()) { const msg = msgs[0]; // eslint-disable-next-line no-warning-comments -- TODO // TODO: Handles script verify messages that are reported only on a specific path. // if (msgs.length < scriptMessages.length) { // } adjustScriptMessages.push(msg); } scriptMessages = [adjustScriptMessages]; } const templateMessages = microTemplateProcessor.postprocess( [messages[0]], filename, ); return [].concat( ...filterDuplicateMessages([templateMessages, ...scriptMessages]), ); }, supportsAutofix: microTemplateProcessor.supportsAutofix, meta: { name: `${baseMeta.name}/script`, version: baseMeta.version, }, };