mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
WIP ESLint rule to require 'data-*' attribute based selectors
This commit is contained in:
parent
44ffb635e9
commit
0fbff9f96f
5 changed files with 1423 additions and 0 deletions
1
libraries/eslint-require-data-selectors/.gitignore
vendored
Normal file
1
libraries/eslint-require-data-selectors/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
1239
libraries/eslint-require-data-selectors/package-lock.json
generated
Normal file
1239
libraries/eslint-require-data-selectors/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
15
libraries/eslint-require-data-selectors/package.json
Normal file
15
libraries/eslint-require-data-selectors/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "eslint-require-data-selectors",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node require-data-selectors.test.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// Custom rule docs <https://eslint.org/docs/latest/extend/custom-rule-tutorial>
|
||||
//
|
||||
// Helpful tool 1 <https://astexplorer.net/>:
|
||||
//
|
||||
// document.querySelector("[data-foo]");
|
||||
// window.body.querySelector("[data-foo]", 456, true);
|
||||
//
|
||||
// Helpful tool 2 <https://estools.github.io/esquery/> (querying for `[id.name="bar"]`):
|
||||
//
|
||||
// document.querySelector("[data-foo]");
|
||||
// window.body.querySelectorAll("[data-foo] [foo]").closest(null).matches("#bar", 123);
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Enforce that CSS selector strings used with `querySelector`, `querySelectorAll`, `closest`, and `matches` only contains `data-*` attributes and disallow `getElementsBy*` APIs.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create: (context) => ({
|
||||
// After <https://github.com/estools/esquery/issues/132> was fixed, we were able to add the `> ` direct child combinator to the start of the `:has()` pseudo-selector (before `MemberExpression`) which was needed for better correctness. But this alteration hasn't been tested yet. This comment can be deleted after testing it.
|
||||
"CallExpression:has(> MemberExpression > Identifier[name=/^querySelector(All)?|closest|matches$/]) > .arguments:first-child:last-child[raw=/^\".*\"$/]": (node) => {
|
||||
const split = deepAssemble(buildParenthesesTree(node.value));
|
||||
if (split === undefined) {
|
||||
context.report({
|
||||
node,
|
||||
message: "Unbalanced parentheses in CSS selector string.",
|
||||
});
|
||||
} else {
|
||||
// Split between selector combinators
|
||||
const list = node.value.split(/ +| *> *| *~ *| *\+ *|(?<=.)(?=\[data-.+\])/).filter((s) => s !== "");
|
||||
// console.log(list);
|
||||
const valid = list.every((s) => /^\[data-[^\]]+\]$/.test(s));
|
||||
|
||||
if (!valid) {
|
||||
context.report({
|
||||
node,
|
||||
message: "CSS selector strings should only contain `data-*` attributes and avoid tag type, class, ID, or non-data attribute selectors.",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
// TODO: Disallow `getElements?By*` APIs.
|
||||
}),
|
||||
};
|
||||
|
||||
function* cartesian_product(head, ...tail) {
|
||||
const remainder = tail.length > 0 ? cartesian_product(...tail) : [[]];
|
||||
for (let r of remainder) for (let h of head) yield [h, ...r];
|
||||
}
|
||||
|
||||
function deepAssemble(sequence) {
|
||||
const assembled = assemble(sequence);
|
||||
console.log(assembled);
|
||||
console.log("============");
|
||||
|
||||
assembled.forEach((group) => {
|
||||
// TODO: Flatten contiguous arrays
|
||||
|
||||
// group.forEach((item) => {
|
||||
// if (Array.isArray(item)) {
|
||||
// const result = assemble(item);
|
||||
// result.forEach((x) => {
|
||||
// console.log("SUB:", x);
|
||||
// })
|
||||
// } else {
|
||||
// console.log("TOP:", item);
|
||||
// }
|
||||
// });
|
||||
console.log(group);
|
||||
});
|
||||
}
|
||||
|
||||
function assemble(sequence) {
|
||||
const output = [];
|
||||
|
||||
const flatSequence = sequence.flat();
|
||||
for (let i = 0; i < flatSequence.length; i++) {
|
||||
const item = flatSequence[i];
|
||||
const previous = output[output.length - 1];
|
||||
const previousLast = previous?.[previous.length - 1];
|
||||
|
||||
// Append to the last group if this item is an array, the previous group's last item was an array, or the previous group's last item ended with an open parenthesis
|
||||
if (Array.isArray(item) || Array.isArray(previousLast) || previousLast?.endsWith("(")) {
|
||||
output[output.length - 1].push(item);
|
||||
} else {
|
||||
output.push([item]);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function buildParenthesesTree(givenString) {
|
||||
// Preprocess commas for multiple selectors, but be careful about commas within pseudo-selectors:
|
||||
//
|
||||
// a, div:not(.fun, .boring), img, foo > .bar ~ :has(> div, span:is([data-red], [data-blue] .bold:not(&.italic), .italic)), qux:not(.foo):not(.bar)
|
||||
// ================================================================================================================================================
|
||||
// a, div:not( ), img, foo > .bar ~ :has( ), qux:not( ):not( )
|
||||
// .fun, .boring > div, span:is( ) .foo .bar
|
||||
// [data-red], [data-blue] .bold:not( ), .italic
|
||||
// &.italic
|
||||
let balancedCount = 0;
|
||||
let stringGroups = [""];
|
||||
|
||||
const balanced = Array.from(givenString).every((c) => {
|
||||
if (c === ")") balancedCount -= 1;
|
||||
if (balancedCount < 0) return false;
|
||||
|
||||
if (c === "(" && balancedCount === 0) {
|
||||
stringGroups[stringGroups.length - 1] += "(";
|
||||
stringGroups.push("");
|
||||
} else if (c === ")" && balancedCount === 0) {
|
||||
stringGroups.push(")");
|
||||
} else {
|
||||
stringGroups[stringGroups.length - 1] += c;
|
||||
}
|
||||
|
||||
if (c === "(") balancedCount += 1;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!balanced || balancedCount !== 0) return undefined;
|
||||
|
||||
return stringGroups.map((text, i) => i % 2 === 1 ? buildParenthesesTree(text) : text.split(/ *, */));
|
||||
}
|
||||
|
||||
debugger;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
const { RuleTester } = require("eslint");
|
||||
const myRule = require("./require-data-selectors");
|
||||
|
||||
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
|
||||
|
||||
ruleTester.run(
|
||||
"require-data-selectors",
|
||||
myRule,
|
||||
{
|
||||
valid: [
|
||||
{ code: `document.querySelector("[data-foo]")` },
|
||||
{ code: `document.querySelector("[data-foo] [data-bar]")` },
|
||||
{ code: `document.querySelector("[data-foo] > [data-bar]")` },
|
||||
{ code: `document.querySelector("[data-foo]>[data-bar]")` },
|
||||
{ code: `document.querySelector("[data-foo] ~ [data-bar]")` },
|
||||
{ code: `document.querySelector("[data-foo]~[data-bar]")` },
|
||||
{ code: `document.querySelector("[data-foo] + [data-bar]")` },
|
||||
{ code: `document.querySelector("[data-foo]+[data-bar]")` },
|
||||
{ code: `document.querySelector(" [data-foo][data-bar] ")` },
|
||||
],
|
||||
invalid: [
|
||||
{ code: `document.querySelector(".foo")`, errors: 1 },
|
||||
{ code: `document.querySelector("div")`, errors: 1 },
|
||||
{ code: `document.querySelector("div.foo")`, errors: 1 },
|
||||
{ code: `document.querySelector("#foo")`, errors: 1 },
|
||||
{ code: `document.querySelector("div#foo")`, errors: 1 },
|
||||
{ code: `document.querySelector("div#foo.bar")`, errors: 1 },
|
||||
{ code: `document.querySelector("foo[data-bar]")`, errors: 1 },
|
||||
{ code: `document.querySelector("[foo][data-bar]")`, errors: 1 },
|
||||
{ code: `document.querySelector("[title]")`, errors: 1 },
|
||||
{ code: `document.querySelector("foo[title]")`, errors: 1 },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
console.log("All tests passed!");
|
Loading…
Add table
Add a link
Reference in a new issue