eslint-plugin-sql-template
Version:
ESLint plugin with rules for using the `sql` template tag on raw SQL queries
110 lines (89 loc) • 2.99 kB
JavaScript
'use strict';
/**
* Helper function to check if an expression contains a variable.
*/
function containsVariableExpression(expression) {
if (!expression) return false;
if (['Identifier', 'CallExpression', 'MemberExpression'].includes(expression.type)) {
return true;
}
if (expression.type === 'ConditionalExpression') {
return containsVariableExpression(expression.consequent) || containsVariableExpression(expression.alternate);
}
if (expression.type === 'TemplateLiteral') {
return expression.expressions.some(containsVariableExpression);
}
return false;
}
/**
* Helper function to check if a node has a parent that is a template literal.
*/
function hasParentTemplateLiteral(node) {
if (!node?.parent) return false;
if (node.parent.type === 'TemplateLiteral') {
return true;
}
return hasParentTemplateLiteral(node.parent);
}
/**
* SQL starting keywords to detect inside the template literal.
*/
const sqlKeywords = /^`\s*(SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WITH|GRANT|BEGIN|DROP)\s/i;
/**
* Rule definition.
*/
module.exports = {
meta: {
type: 'suggestion',
hasSuggestions: true,
fixable: 'code',
docs: {
description: 'Enforce safe SQL query handling using tagged templates',
recommended: false,
url: 'https://github.com/uphold/eslint-plugin-sql-template#rules'
},
messages: {
missingSqlTag: 'Use the `sql` tagged template literal for raw queries'
},
schema: []
},
create(context) {
return {
TemplateLiteral(node) {
// Only check interpolated template literals.
if (node?.type !== 'TemplateLiteral' || node.expressions.length === 0) {
return;
}
// Skip if the template literal has in it's chain a parent that is a TemplateLiteral.
if (hasParentTemplateLiteral(node)) {
return;
}
// Skip if the template literal is already tagged with `sql`.
if (node.parent.type === 'TaggedTemplateExpression' && node.parent.tag.name === 'sql') {
return;
}
// Check if the template literal has SQL.
const hasSQL = sqlKeywords.test(context.sourceCode.getText(node));
// Recursively check if any expression is a variable (Identifier, MemberExpression, or nested TemplateLiteral)
const hasVariableExpression = node.expressions.some(containsVariableExpression);
if (hasSQL && hasVariableExpression) {
context.report({
node,
messageId: 'missingSqlTag',
suggest: [
{
desc: 'Wrap with sql tag',
fix(fixer) {
if (node.parent?.type === 'TaggedTemplateExpression') {
return fixer.replaceText(node.parent.tag, 'sql');
}
return fixer.insertTextBefore(node, 'sql');
}
}
]
});
}
}
};
}
};