Skip to content

Commit

Permalink
Merge pull request #92 from 5app/next
Browse files Browse the repository at this point in the history
chore: release latest
  • Loading branch information
MrSwitch authored Sep 2, 2024
2 parents 1b4ba49 + 4b9f898 commit 6f816e5
Show file tree
Hide file tree
Showing 12 changed files with 546 additions and 25 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
## [1.12.1](https://github.com/5app/eslint-plugin-sequel/compare/v1.12.0...v1.12.1) (2024-08-01)


### Bug Fixes

* **no-shorthand-offset:** fix perf issue with lookbehind assertion, noissue ([8c62032](https://github.com/5app/eslint-plugin-sequel/commit/8c6203214c8792cd83eb5b814f63e234e6c386a9))

# [1.12.0](https://github.com/5app/eslint-plugin-sequel/compare/v1.11.0...v1.12.0) (2024-08-01)


### Features

* **no-shorthand-offset:** prevent 'LIMIT offset, count' syntax ([94e0409](https://github.com/5app/eslint-plugin-sequel/commit/94e04098c52133349acd3476fdc541e1dfe714dc))

# [1.11.0](https://github.com/5app/eslint-plugin-sequel/compare/v1.10.0...v1.11.0) (2024-07-15)


### Features

* **no-backticks:** new rule to prevent incompatible backticks, noissue ([0181fce](https://github.com/5app/eslint-plugin-sequel/commit/0181fce8ff78e595829f248e854d63cc608ded49))

# [1.10.0](https://github.com/5app/eslint-plugin-sequel/compare/v1.9.10...v1.10.0) (2024-07-08)


### Features

* **allowed-functions:** rule allowed-functions or rather disallowed, fixes [#86](https://github.com/5app/eslint-plugin-sequel/issues/86) ([d2be98c](https://github.com/5app/eslint-plugin-sequel/commit/d2be98c4766cb5f47b0ab9e8f41675a9c952409f))

## [1.9.10](https://github.com/5app/eslint-plugin-sequel/compare/v1.9.9...v1.9.10) (2023-07-19)


Expand Down
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,47 @@ Then configure the rules you want to use under the rules section.
```json
{
"rules": {
"sequel/function-case": 2,
"sequel/indent": [2, "tab"],
"sequel/max-placeholders": [2, {"max": 3}],
"sequel/no-eol-command": [2, {"allowOnOwnLine": true}],
"sequel/allowed-functions": [
"error"
{"disallow": ["GROUP_CONCAT"]}
],
"sequel/function-case": "error",
"sequel/indent": ["error", "tab"],
"sequel/max-placeholders": [
"error",
{"max": 3}
],
"sequel/no-backticks": "error",
"sequel/no-eol-command": [
"error",
{"allowOnOwnLine": true}
],
"sequel/no-shorthand-all": [
2,
"error",
{"allowQualified": true, "allowCountAll": true}
],
"sequel/no-unsafe-query": 2,
"sequel/spacing": 2
"sequel/no-shorthand-offset": "error",
"sequel/no-unsafe-query": "error",
"sequel/spacing": "error"
}
}
```

## Rules

- `sequel/allowed-functions`: List functions which are **not** allowed
- `disallow`: Array of disallowed SQL functions
- `sequel/function-case`: Makes SQL function names uppercase, e.g. 'SELECT' **fixable**
- `sequel/indent`: Enforces indentation **fixable**
- `'tab'|Number`: Defines the characters to use, where Number is given it uses spaces (default `2`).
- `sequel/max-placeholders`: Placeholders, `?` character, can be hard to read if there are many in the same SQL string.
- `max`: Maximum number of placeholders allowed (default `3`)
- `sequel/no-backticks`: Prevent the use of non-standard backticks to quote identifiers - use quotes, table prefixes on fields, or naming which does not conflict.
- `sequel/no-eol-command`: Avoid ending lines with a SQL command which is always followed by a value.
- `allowOnOwnLine`: Permits the command to appear if it is not preceeded by anything, allowing commands to be easily read.
- `sequel/no-shorthand-all`: Avoid using the ambiguous shorthand all '\*'.
- `allowQualified` (Boolean, default: `false`): Permits qualified shorthand all e.g. `table.*` to get everything from a table.
- `allowCountAll` (Boolean, default: `false`): Permits within `COUNT()` e.g. `COUNT(*)`.
- `sequel/no-shorthand-offset`: Prevent non-standard SQL `LIMIT offset, count`
- `sequel/no-unsafe-query`: Checks whether there are potentially any vulnerable SQL'ish template literals, fix by using SQL placeholders or using [SQL templating formatter](https://www.npmjs.com/search?q=sql%20template)
- `sequel/spacing`: Multiple spaces and tabs should only be used for indentation **fixable**
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-sequel",
"version": "1.9.10",
"version": "1.12.1",
"description": "Eslint rules for inline SQL",
"keywords": [
"eslint",
Expand Down
77 changes: 77 additions & 0 deletions rules/allowed-functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const createSQLTemplateElementHandler = require('../utils/createSQLTemplateElementHandler');
const getLocation = require('../utils/getLocation.js');

/**
* Template Element Handler for Function Names allowed and disallowed
* @param {object} node - Element Node
* @param {object} context - Eslint Context object
* @returns {object|undefined} Returns a format object or undefined
*/
function templateElementHandler(node, context) {
const text = node.value.raw;

const {disallowed} = context.options.at(0);

// Special words?
const regexp = new RegExp(
`\\b(?<funcName>${disallowed.join('|')})\\(`,
'gi'
);

const test = text.matchAll(regexp);

const incidents = [];

for (const match of test) {
const {funcName} = match.groups;

const report = {
messageId: 'isDisallowed',
data: {
funcName,
},
...getLocation(node, match.index, funcName),
};

incidents.push(report);
}

return incidents;
}

/**
* Export `function-case`.
*
* @param {object} context - Eslint Context object
* @returns {object} Object rule
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce allowed and disallowed functions',
category: 'Stylistic Issues',
},
messages: {
isDisallowed: 'Function is disallowed "{{funcName}}"',
},
schema: {
minItems: 1,
maxItems: 1,
items: [
{
type: 'object',
properties: {
disallowed: {
type: 'array',
items: {type: 'string'},
minItems: 1,
},
},
additionalProperties: false,
},
],
},
},
create: createSQLTemplateElementHandler({templateElementHandler}),
};
69 changes: 69 additions & 0 deletions rules/no-backticks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const createSQLTemplateElementHandler = require('../utils/createSQLTemplateElementHandler');

/**
* Template Element Handler for Backticks
* @param {object} node - Element Node
* @returns {object|undefined} Returns a format object or undefined
*/
function templateElementHandler(node) {
const text = node.value.raw;

const backtick = '\\`';

const test = text.matchAll(backtick);

const incidents = [];

for (const match of test) {
// Get the text before the match, and split it into lines
const prefixLines = text.slice(0, match.index).split('\n');
// Extract the last line, everything in the column preceeding the backtick
const prefixLast = prefixLines.pop();
// Derive the column, if the prefix is empty i.e. no multi-line, include the start column of the node
const column =
(prefixLines.length === 0 ? node.loc.start?.column : -1) +
prefixLast.length;
// Count the lines: the text node, and then the lines within the text node before the backtick
const line = node.loc.start?.line + prefixLines.length;

const report = {
messageId: 'isDisallowed',
loc: {
start: {
line,
column,
},
end: {
line,
column: column + backtick.length,
},
},
};

incidents.push(report);
}

return incidents;
}

/**
* Export `no-backticks`.
*
* @param {object} context - Eslint Context object
* @returns {object} Object rule
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Enforce allowed and disallowed backticks in SQL template literals',
category: 'Stylistic Issues',
},
messages: {
isDisallowed:
'Backticks are disallowed as they are incompatible with SQL. Use double quotes instead, prefix identifier names or ammend names of fields stored in the database.',
},
},
create: createSQLTemplateElementHandler({templateElementHandler}),
};
115 changes: 115 additions & 0 deletions rules/no-shorthand-offset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const isSqlQuery = require('../utils/sqlParser.js');
const isTagged = require('../utils/isTagged.js');
const getLocation = require('../utils/getLocation.js');

/**
* no-shorthand-offset
* MySQL permits the non-standard `LIMIT offset, count` syntax, but it is not portable to other SQL databases.
* So this rule will flag the potential use of `LIMIT offset, count` syntax.
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of `LIMIT offset, count` syntax',
category: 'non-standard SQL',
},
messages: {
nonStandardSQL:
'Non-standard SQL syntax `LIMIT offset, count` is not portable',
},
},

/**
* Create `no-shorthand-offset` rule
*
* @param {object} context - Eslint Context object
* @returns {object} Object rule
*/
create(context) {
/**
* Validate node.
* @param {object} templateLiteralNode - Node
* @returns {void}
*/
function validate(templateLiteralNode) {
const {parent} = templateLiteralNode;

// Tagged with a 'SQL' like name?
const tagged = isTagged(parent);

// Join up the parts...
const literal = templateLiteralNode.quasis
.map(quasi => quasi.value.raw)
.join('?');

// Is this something other than a SQL expression?
if (!tagged && !isSqlQuery(literal)) {
return;
}

const incidents = [];

// Loop through the TemplateElements
templateLiteralNode.quasis.forEach((node, index) => {
// Is there a `LIMIT offset,` clause?
const hardcodedShortHandOffset = node.value.raw.match(
/\b(?<prefix>limit(\s+(--.*\n)*)+)(?<body>(\d+|\?)\s*,)/i
);

if (hardcodedShortHandOffset) {
const matchStr = hardcodedShortHandOffset.groups.body;
const matchIndex =
hardcodedShortHandOffset.index +
hardcodedShortHandOffset.groups.prefix.length;

incidents.push({
messageId: 'nonStandardSQL',
...getLocation(node, matchIndex, matchStr),
});
return;
}

// Is there a `LIMIT` clause?
const endsInLimitClause = node.value.raw.match(
/limit(\s+(--.*\n)*)+$/i
);

if (endsInLimitClause && !node.tail) {
// Get the next node in sequence, does that start with a `,`?
const nextNode = templateLiteralNode.quasis[index + 1];

const nextStartsWithComma =
nextNode.value.raw.match(/^(\s*),/i);

// console.log({
// nextNode,
// endsInLimitClause,
// nextStartsWithComma,
// });

if (nextStartsWithComma) {
const strPattern = ',';

// Report the error
const matchIndex =
nextNode.value.raw.indexOf(strPattern);

incidents.push({
messageId: 'nonStandardSQL',
...getLocation(nextNode, matchIndex, strPattern),
});
}
}
});

// console.log(incidents);

incidents.forEach(incident => context.report(incident));
}

return {
TemplateLiteral: validate,
};
},
};
Loading

0 comments on commit 6f816e5

Please sign in to comment.