0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

revert selector changes

This commit is contained in:
Bartek Iwańczuk 2024-12-21 16:28:17 +01:00
parent 6e28ae5425
commit c831535ac9
No known key found for this signature in database
GPG key ID: 0C6BCDDC3B3AD750
6 changed files with 48 additions and 2374 deletions

View file

@ -3,11 +3,6 @@
// @ts-check
import { core, internals } from "ext:core/mod.js";
import {
compileSelector,
parseSelector,
splitSelectors,
} from "ext:cli/40_lint_selector.js";
const {
op_lint_get_rule,
@ -391,238 +386,6 @@ function getString(strTable, id) {
return name;
}
/** @implements {MatchContext} */
class MatchCtx {
/**
* @param {AstContext["buf"]} buf
* @param {AstContext["strTable"]} strTable
*/
constructor(buf, strTable) {
this.buf = buf;
this.strTable = strTable;
}
/**
* @param {number} offset
* @returns {number}
*/
getParent(offset) {
return readU32(this.buf, offset + 1);
}
/**
* @param {number} offset
* @returns {number}
*/
getType(offset) {
return this.buf[offset];
}
/**
* @param {number} offset
* @param {number[]} propIds
* @param {number} idx
* @returns {unknown}
*/
getAttrPathValue(offset, propIds, idx) {
const { buf } = this;
offset = findPropOffset(buf, offset, propIds[idx]);
if (offset === -1) return undefined;
const _prop = buf[offset++];
const kind = buf[offset++];
if (kind === PropFlags.Ref) {
const value = readU32(buf, offset);
// Checks need to end with a value, not a node
if (idx === propIds.length - 1) return undefined;
return this.getAttrPathValue(value, propIds, idx + 1);
} else if (kind === PropFlags.RefArr) {
// FIXME
const _count = readU32(buf, offset);
offset += 4;
}
// Cannot traverse into primitives further
if (idx < propIds.length - 1) return undefined;
if (kind === PropFlags.String) {
const s = readU32(buf, offset);
return getString(this.strTable, s);
} else if (kind === PropFlags.Bool) {
return buf[offset] === 1;
} else if (kind === PropFlags.Null) {
return null;
} else if (kind === PropFlags.Undefined) {
return undefined;
}
return undefined;
}
/**
* @param {number} offset
* @param {number[]} propIds
* @param {number} idx
* @returns {boolean}
*/
hasAttrPath(offset, propIds, idx) {
const { buf } = this;
offset = findPropOffset(buf, offset, propIds[idx]);
// console.log("attr path", offset, propIds, idx);
if (offset === -1) return false;
if (idx === propIds.length - 1) return true;
const prop = buf[offset++];
const kind = buf[offset++];
if (kind === PropFlags.Ref) {
const value = readU32(buf, offset);
return this.hasAttrPath(value, propIds, idx + 1);
} else if (kind === PropFlags.RefArr) {
const count = readU32(buf, offset);
offset += 4;
// FIXME
}
// Primitives cannot be traversed further. This means we
// didn't found the attribute.
if (idx < propIds.length - 1) return false;
return true;
}
/**
* @param {number} offset
* @returns {number}
*/
getFirstChild(offset) {
const { buf } = this;
// type + parentId + SpanLo + SpanHi
offset += 1 + 4 + 4 + 4;
const count = buf[offset++];
for (let i = 0; i < count; i++) {
const _prop = buf[offset++];
const kind = buf[offset++];
switch (kind) {
case PropFlags.Ref: {
const v = readU32(buf, offset);
offset += 4;
return v;
}
case PropFlags.RefArr: {
const len = readU32(buf, offset);
offset += 4;
for (let j = 0; j < len; j++) {
const v = readU32(buf, offset);
offset += 4;
return v;
}
return len;
}
case PropFlags.String:
offset += 4;
break;
case PropFlags.Bool:
offset++;
break;
case PropFlags.Null:
case PropFlags.Undefined:
break;
}
}
return -1;
}
/**
* @param {number} offset
* @returns {number}
*/
getLastChild(offset) {
const { buf } = this;
// type + parentId + SpanLo + SpanHi
offset += 1 + 4 + 4 + 4;
let last = -1;
const count = buf[offset++];
for (let i = 0; i < count; i++) {
const _prop = buf[offset++];
const kind = buf[offset++];
switch (kind) {
case PropFlags.Ref: {
const v = readU32(buf, offset);
offset += 4;
last = v;
break;
}
case PropFlags.RefArr: {
const len = readU32(buf, offset);
offset += 4;
for (let j = 0; j < len; j++) {
const v = readU32(buf, offset);
last = v;
offset += 4;
}
break;
}
case PropFlags.String:
offset += 4;
break;
case PropFlags.Bool:
offset++;
break;
case PropFlags.Null:
case PropFlags.Undefined:
break;
}
}
return last;
}
/**
* @param {number} id
* @returns {number[]}
*/
getSiblings(id) {
const { buf } = this;
const result = findChildOffset(buf, id);
// Happens for program nodes
if (result === null) return [];
if (result[1] === -1) {
return [id];
}
let offset = result[0];
const count = readU32(buf, offset);
offset += 4;
/** @type {number[]} */
const out = [];
for (let i = 0; i < count; i++) {
const v = readU32(buf, offset);
offset += 4;
out.push(v);
}
return out;
}
}
/**
* @param {Uint8Array} buf
* @param {AstContext} buf
@ -703,7 +466,6 @@ function createAstContext(buf) {
strByType,
typeByStr,
propByStr,
matcher: new MatchCtx(buf, strTable),
};
setNodeGetters(ctx);
@ -756,37 +518,33 @@ export function runPluginsForFile(fileName, serializedAst) {
isExit = true;
key = key.slice(0, -":exit".length);
}
const selectors = splitSelectors(key);
for (let j = 0; j < selectors.length; j++) {
const fnKey = selectors[j] + (isExit ? ":exit" : "");
let info = bySelector.get(fnKey);
if (info === undefined) {
info = { enter: NOOP, exit: NOOP };
bySelector.set(fnKey, info);
let info = bySelector.get(key);
if (info === undefined) {
info = { enter: NOOP, exit: NOOP };
bySelector.set(key, info);
}
const prevFn = isExit ? info.exit : info.enter;
/**
* @param {*} node
*/
const wrapped = (node) => {
prevFn(node);
try {
fn(node);
} catch (err) {
throw new Error(`Visitor "${name}" of plugin "${id}" errored`, {
cause: err,
});
}
const prevFn = isExit ? info.exit : info.enter;
};
/**
* @param {*} node
*/
const wrapped = (node) => {
prevFn(node);
try {
fn(node);
} catch (err) {
throw new Error(`Visitor "${name}" of plugin "${id}" errored`, {
cause: err,
});
}
};
if (isExit) {
info.exit = wrapped;
} else {
info.enter = wrapped;
}
if (isExit) {
info.exit = wrapped;
} else {
info.enter = wrapped;
}
}
@ -803,31 +561,24 @@ export function runPluginsForFile(fileName, serializedAst) {
}
}
// Create selectors
/** @type {TransformerFn} */
const toElem = (str) => {
const id = ctx.typeByStr.get(str);
if (id === undefined) throw new Error(`Unknown elem: ${str}`);
return id;
};
/** @type {TransformerFn} */
const toAttr = (str) => {
const id = ctx.propByStr.get(str);
if (id === undefined) throw new Error(`Unknown elem: ${str}`);
return id;
};
/** @type {CompiledVisitor[]} */
const visitors = [];
for (const [sel, info] of bySelector.entries()) {
// Selectors are already split here.
// TODO: Avoid array allocation (not sure if that matters)
const parsed = parseSelector(sel, toElem, toAttr)[0];
const matcher = compileSelector(parsed);
// This will make more sense once selectors land as it's faster
// to precompile them once upfront.
// Convert the visiting element name to a number. This number
// is part of the serialized buffer and comparing a single number
// is quicker than strings.
const elemId = ctx.typeByStr.get(sel) ?? -1;
visitors.push({
info,
matcher,
// Check if we should call this visitor
matcher: (offset) => {
const type = ctx.buf[offset];
return type === elemId;
},
});
}
@ -862,18 +613,18 @@ function traverse(ctx, visitors, offset) {
for (let i = 0; i < visitors.length; i++) {
const v = visitors[i];
if (v.info.enter === NOOP) {
continue;
}
if (v.matcher(offset)) {
if (v.info.exit !== NOOP) {
if (exits === null) {
exits = [v.info.exit];
} else {
exits.push(v.info.exit);
}
}
if (v.matcher(ctx.matcher, offset)) {
const node = /** @type {*} */ (getNode(ctx, offset));
v.info.enter(node);
if (exits === null) {
exits = [v.info.exit];
} else {
exits.push(v.info.exit);
if (v.info.enter !== NOOP) {
const node = /** @type {*} */ (getNode(ctx, offset));
v.info.enter(node);
}
}
}

View file

@ -1,999 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// @ts-check
/** @typedef {import("./40_lint_types.d.ts").LintState} LintState */
/** @typedef {import("./40_lint_types.d.ts").AstContext} AstContext */
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchCtx */
/** @typedef {import("./40_lint_types.d.ts").AttrExists} AttrExists */
/** @typedef {import("./40_lint_types.d.ts").AttrBin} AttrBin */
/** @typedef {import("./40_lint_types.d.ts").AttrSelector} AttrSelector */
/** @typedef {import("./40_lint_types.d.ts").ElemSelector} ElemSelector */
/** @typedef {import("./40_lint_types.d.ts").PseudoNthChild} PseudoNthChild */
/** @typedef {import("./40_lint_types.d.ts").PseudoHas} PseudoHas */
/** @typedef {import("./40_lint_types.d.ts").PseudoNot} PseudoNot */
/** @typedef {import("./40_lint_types.d.ts").Relation} SRelation */
/** @typedef {import("./40_lint_types.d.ts").Selector} Selector */
/** @typedef {import("./40_lint_types.d.ts").SelectorParseCtx} SelectorParseCtx */
/** @typedef {import("./40_lint_types.d.ts").NextFn} NextFn */
/** @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn */
/** @typedef {import("./40_lint_types.d.ts").TransformFn} Transformer */
const Char = {
Tab: 9,
Space: 32,
Bang: 33,
DoubleQuote: 34,
Quote: 39,
BraceOpen: 40,
BraceClose: 41,
Plus: 43,
Comma: 44,
Minus: 45,
Dot: 46,
Slash: 47,
n0: 49,
n9: 57,
Colon: 58,
Less: 60,
Equal: 61,
Greater: 62,
A: 65,
Z: 90,
BracketOpen: 91,
BackSlash: 92,
BracketClose: 93,
Underscore: 95,
a: 97,
z: 122,
Tilde: 126,
};
export const Token = {
EOF: 0,
Word: 1,
Space: 2,
Op: 3,
Colon: 4,
Comma: 7,
BraceOpen: 8,
BraceClose: 9,
BracketOpen: 10,
BracketClose: 11,
String: 12,
Number: 13,
Bool: 14,
Null: 15,
Undefined: 16,
Dot: 17,
Minus: 17,
};
export const BinOp = {
/** [attr="value"] or [attr=value] */
Equal: 1,
/** [attr!="value"] or [attr!=value] */
NotEqual: 2,
/** [attr>1] */
Greater: 3,
/** [attr>=1] */
GreaterThan: 4,
/** [attr<1] */
Less: 5,
/** [attr<=1] */
LessThan: 6,
Tilde: 7,
Plus: 8,
Space: 9,
};
/**
* @param {string} s
* @returns {number}
*/
function getAttrOp(s) {
switch (s) {
case "=":
return BinOp.Equal;
case "!=":
return BinOp.NotEqual;
case ">":
return BinOp.Greater;
case ">=":
return BinOp.GreaterThan;
case "<":
return BinOp.Less;
case "<=":
return BinOp.LessThan;
case "~":
return BinOp.Tilde;
case "+":
return BinOp.Plus;
default:
throw new Error(`Unknown attribute operator: '${s}'`);
}
}
export class Lexer {
token = Token.Word;
start = 0;
end = 0;
ch = 0;
i = -1;
value = "";
/**
* @param {string} input
*/
constructor(input) {
this.input = input;
this.step();
this.next();
}
/**
* @param {number} token
*/
expect(token) {
if (this.token !== token) {
throw new Error(
`Expected token '${token}', but got '${this.token}'.\n\n${this.input}\n${
" ".repeat(this.i)
}^`,
);
}
}
/**
* @param {number} token
*/
readAsWordUntil(token) {
const s = this.i;
while (this.token !== Token.EOF && this.token !== token) {
this.next();
}
this.start = s;
this.end = this.i - 1;
this.value = this.getSlice();
}
getSlice() {
return this.input.slice(this.start, this.end);
}
step() {
this.i++;
if (this.i >= this.input.length) {
this.ch = -1;
} else {
this.ch = this.input.charCodeAt(this.i);
}
}
next() {
this.value = "";
if (this.i >= this.input.length) {
this.token = Token.EOF;
return;
}
// console.log(
// "NEXT",
// this.input,
// this.i,
// JSON.stringify(String.fromCharCode(this.ch)),
// );
while (true) {
switch (this.ch) {
case Char.Space:
while (this.isWhiteSpace()) {
this.step();
}
// Check if space preceeded operator
if (this.isOpContinue()) {
continue;
}
this.token = Token.Space;
return;
case Char.BracketOpen:
this.token = Token.BracketOpen;
this.step();
return;
case Char.BracketClose:
this.token = Token.BracketClose;
this.step();
return;
case Char.BraceOpen:
this.token = Token.BraceOpen;
this.step();
return;
case Char.BraceClose:
this.token = Token.BraceClose;
this.step();
return;
case Char.Colon:
this.token = Token.Colon;
this.step();
return;
case Char.Comma:
this.token = Token.Comma;
this.step();
return;
case Char.Dot:
this.token = Token.Dot;
this.step();
return;
case Char.Minus:
this.token = Token.Minus;
this.step();
return;
case Char.Plus:
case Char.Tilde:
case Char.Greater:
case Char.Equal:
case Char.Less:
case Char.Bang: {
this.token = Token.Op;
this.start = this.i;
this.step();
while (this.isOpContinue()) {
this.step();
}
this.end = this.i;
this.value = this.getSlice();
// Consume remaining space
while (this.isWhiteSpace()) {
this.step();
}
return;
}
case Char.Quote:
case Char.DoubleQuote: {
this.token = Token.String;
const ch = this.ch;
this.step();
this.start = this.i;
while (this.ch > 0 && this.ch !== ch) {
this.step();
}
this.end = this.i;
this.value = this.getSlice();
this.step();
return;
}
default:
this.start = this.i;
this.step();
while (this.isWordContinue()) {
this.step();
}
this.end = this.i;
this.value = this.getSlice();
this.token = Token.Word;
return;
}
}
}
isWordContinue() {
const ch = this.ch;
switch (ch) {
case Char.Minus:
case Char.Underscore:
return true;
default:
return (ch >= Char.a && ch <= Char.z) ||
(ch >= Char.A && ch <= Char.Z) ||
(ch >= Char.n0 && ch <= Char.n9);
}
}
isOpContinue() {
const ch = this.ch;
switch (ch) {
case Char.Equal:
case Char.Bang:
case Char.Greater:
case Char.Less:
case Char.Tilde:
case Char.Plus:
return true;
default:
return false;
}
}
isWhiteSpace() {
return this.ch === Char.Space || this.ch === Char.Tab;
}
}
const NUMBER_REG = /^(\d+\.)?\d+$/;
const BIGINT_REG = /^\d+n$/;
/**
* @param {string} raw
* @returns {any}
*/
function getFromRawValue(raw) {
switch (raw) {
case "true":
return true;
case "false":
return false;
case "null":
return null;
case "undefined":
return undefined;
default:
if (raw.startsWith("'") && raw.endsWith("'")) {
if (raw.length === 2) return "";
return raw.slice(1, -1);
} else if (raw.startsWith('"') && raw.endsWith('"')) {
if (raw.length === 2) return "";
return raw.slice(1, -1);
} else if (raw.startsWith("/")) {
const end = raw.lastIndexOf("/");
if (end === -1) throw new Error(`Invalid RegExp pattern: ${raw}`);
const pattern = raw.slice(1, end);
const flags = end < raw.length - 1 ? raw.slice(end + 1) : undefined;
return new RegExp(pattern, flags);
} else if (NUMBER_REG.test(raw)) {
return Number(raw);
} else if (BIGINT_REG.test(raw)) {
return BigInt(raw.slice(0, -1));
}
return raw;
}
}
export const ELEM_NODE = 1;
export const RELATION_NODE = 2;
export const ATTR_EXISTS_NODE = 3;
export const ATTR_BIN_NODE = 4;
export const PSEUDO_NTH_CHILD = 5;
export const PSEUDO_HAS = 6;
export const PSEUDO_NOT = 7;
export const PSEUDO_FIRST_CHILD = 8;
export const PSEUDO_LAST_CHILD = 9;
/**
* Parse out all unique selectors of a selector list.
* @param {string} input
* @returns {string[]}
*/
export function splitSelectors(input) {
/** @type {string[]} */
const out = [];
let last = 0;
let depth = 0;
for (let i = 0; i < input.length; i++) {
const ch = input.charCodeAt(i);
switch (ch) {
case Char.BraceOpen:
depth++;
break;
case Char.BraceClose:
depth--;
break;
case Char.Comma:
if (depth === 0) {
out.push(input.slice(last, i).trim());
last = i + 1;
}
break;
}
}
if (last < input.length - 1) {
out.push(input.slice(last).trim());
}
return out;
}
/**
* @param {string} input
* @param {Transformer} toElem
* @param {Transformer} toAttr
* @returns {Selector[]}
*/
export function parseSelector(input, toElem, toAttr) {
/** @type {Selector[]} */
const result = [];
/** @type {Selector[]} */
const stack = [[]];
const lex = new Lexer(input);
// Some subselectors like `:nth-child(.. of <selector>)` must have
// a single selector instead of selector list.
let throwOnComma = false;
while (lex.token !== Token.EOF) {
const current = /** @type {Selector} */ (stack.at(-1));
if (lex.token === Token.Word) {
const value = lex.value;
const wildcard = value === "*";
const elem = !wildcard ? toElem(value) : 0;
current.push({
type: ELEM_NODE,
elem,
wildcard,
});
lex.next();
continue;
} else if (lex.token === Token.Space) {
lex.next();
if (lex.token === Token.Word) {
current.push({
type: RELATION_NODE,
op: BinOp.Space,
});
}
continue;
} else if (lex.token === Token.BracketOpen) {
lex.next();
lex.expect(Token.Word);
// Check for value comparison
const prop = [toAttr(lex.value)];
lex.next();
while (lex.token === Token.Dot) {
lex.next();
lex.expect(Token.Word);
prop.push(toAttr(lex.value));
lex.next();
}
if (lex.token === Token.Op) {
const op = getAttrOp(lex.value);
lex.readAsWordUntil(Token.BracketClose);
const value = getFromRawValue(lex.value);
current.push({ type: ATTR_BIN_NODE, prop, op, value });
} else {
current.push({
type: ATTR_EXISTS_NODE,
prop,
});
}
lex.expect(Token.BracketClose);
lex.next();
continue;
} else if (lex.token === Token.Colon) {
lex.next();
lex.expect(Token.Word);
switch (lex.value) {
case "first-child":
current.push({
type: PSEUDO_FIRST_CHILD,
});
break;
case "last-child":
current.push({
type: PSEUDO_LAST_CHILD,
});
break;
case "nth-child": {
lex.next();
lex.expect(Token.BraceOpen);
lex.next();
let mul = 1;
let repeat = false;
let step = 0;
if (lex.token === Token.Minus) {
mul = -1;
lex.next();
}
lex.expect(Token.Word);
const value = lex.getSlice();
if (value.endsWith("n")) {
repeat = true;
step = +value.slice(0, -1) * mul;
} else {
step = +value * mul;
}
lex.next();
/** @type {PseudoNthChild} */
const node = {
type: PSEUDO_NTH_CHILD,
of: null,
op: null,
step,
stepOffset: 0,
repeat,
};
current.push(node);
if (lex.token === Token.Space) lex.next();
if (lex.token !== Token.BraceClose) {
if (lex.token === Token.Op) {
node.op = lex.value;
lex.next();
if (lex.token === Token.Space) lex.next();
} else if (lex.token === Token.Minus) {
node.op = "-";
lex.next();
if (lex.token === Token.Space) {
lex.next();
}
}
lex.expect(Token.Word);
node.stepOffset = +lex.value;
lex.next();
if (lex.token !== Token.BraceClose) {
lex.next(); // Space
if (lex.token === Token.Word) {
if (/** @type {string} */ (lex.value) !== "of") {
throw new Error(
`Expected 'of' keyword in ':nth-child' but got: ${lex.value}`,
);
}
lex.next();
lex.expect(Token.Space);
lex.next();
throwOnComma = true;
stack.push([]);
}
continue;
}
lex.expect(Token.BraceClose);
} else if (!node.repeat) {
// :nth-child(2) -> step is actually stepOffset
node.stepOffset = node.step - 1;
node.step = 0;
}
lex.next();
continue;
}
case "has":
case "where":
case "is": {
lex.next();
lex.expect(Token.BraceOpen);
lex.next();
current.push({
type: PSEUDO_HAS,
selectors: [],
});
stack.push([]);
continue;
}
case "not": {
lex.next();
lex.expect(Token.BraceOpen);
lex.next();
current.push({
type: PSEUDO_NOT,
selectors: [],
});
stack.push([]);
continue;
}
default:
throw new Error(`Unknown pseudo selector: '${lex.value}'`);
}
} else if (lex.token === Token.Comma) {
if (throwOnComma) {
throw new Error(`Multiple selector arguments not supported here`);
}
lex.next();
if (lex.token === Token.Space) {
lex.next();
}
popSelector(result, stack);
stack.push([]);
continue;
} else if (lex.token === Token.BraceClose) {
throwOnComma = false;
popSelector(result, stack);
} else if (lex.token === Token.Op) {
current.push({
type: RELATION_NODE,
op: getAttrOp(lex.value),
});
}
lex.next();
}
if (stack.length > 0) {
result.push(stack[0]);
}
return result;
}
/**
* @param {Selector[]} result
* @param {Selector[]} stack
*/
function popSelector(result, stack) {
const sel = /** @type {Selector} */ (stack.pop());
if (stack.length === 0) {
result.push(sel);
stack.push([]);
} else {
const prev = /** @type {Selector} */ (stack.at(-1));
if (prev.length === 0) {
throw new Error(`Empty selector`);
}
const node = prev.at(-1);
if (node === undefined) {
throw new Error(`Empty node`);
}
if (node.type === PSEUDO_NTH_CHILD) {
node.of = sel;
} else if (node.type === PSEUDO_HAS || node.type === PSEUDO_NOT) {
node.selectors.push(sel);
} else {
throw new Error(`Multiple selectors not allowed here`);
}
}
}
const TRUE_FN = () => {
return true;
};
/**
* @param {Selector} selector
* @returns {MatcherFn}
*/
export function compileSelector(selector) {
/** @type {MatcherFn} */
let fn = TRUE_FN;
for (let i = 0; i < selector.length; i++) {
const node = selector[i];
switch (node.type) {
case ELEM_NODE:
fn = matchElem(node, fn);
break;
case RELATION_NODE:
switch (node.op) {
case BinOp.Space:
fn = matchDescendant(fn);
break;
case BinOp.Greater:
fn = matchChild(fn);
break;
case BinOp.Plus:
fn = matchAdjacent(fn);
break;
case BinOp.Tilde:
fn = matchFollowing(fn);
break;
default:
throw new Error(`Unknown relation op ${node.op}`);
}
break;
case ATTR_EXISTS_NODE:
fn = matchAttrExists(node, fn);
break;
case ATTR_BIN_NODE:
fn = matchAttrBin(node, fn);
break;
case PSEUDO_FIRST_CHILD:
fn = matchFirstChild(fn);
break;
case PSEUDO_LAST_CHILD:
fn = matchLastChild(fn);
break;
case PSEUDO_NTH_CHILD:
fn = matchNthChild(node, fn);
break;
case PSEUDO_HAS:
// FIXME
// fn = matchIs(part, fn);
throw new Error("TODO: :has");
case PSEUDO_NOT:
fn = matchNot(node.selectors, fn);
break;
default:
// @ts-ignore error handling
console.log(node);
throw new Error(`Unknown selector node`);
}
}
return fn;
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchFirstChild(next) {
return (ctx, id) => {
const parent = ctx.getParent(id);
const first = ctx.getFirstChild(parent);
return first === id && next(ctx, first);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchLastChild(next) {
return (ctx, id) => {
const parent = ctx.getParent(id);
const last = ctx.getLastChild(parent);
return last === id && next(ctx, id);
};
}
/**
* @param {PseudoNthChild} node
* @param {number} i
* @returns {number}
*/
function getNthAnB(node, i) {
const n = node.step * i;
if (node.op === null) return n;
switch (node.op) {
case "+":
return n + node.stepOffset;
case "-":
return n - node.stepOffset;
default:
throw new Error("Not supported nth-child operator: " + node.op);
}
}
/**
* @param {PseudoNthChild} node
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchNthChild(node, next) {
const ofSelector = node.of !== null ? compileSelector(node.of) : TRUE_FN;
return (ctx, id) => {
const siblings = ctx.getSiblings(id);
const idx = siblings.indexOf(id);
if (!node.repeat) {
return idx === node.stepOffset && next(ctx, id);
}
for (let i = 0; i < siblings.length; i++) {
const n = getNthAnB(node, i);
if (id === n) {
if (node.of !== null && !ofSelector(ctx, id)) {
continue;
} else if (next(ctx, id)) {
return true;
}
} else if (n > idx) {
return false;
}
}
return false;
};
}
/**
* @param {Selector[]} selectors
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchNot(selectors, next) {
/** @type {MatcherFn[]} */
const compiled = [];
for (let i = 0; i < selectors.length; i++) {
const sel = selectors[i];
compiled.push(compileSelector(sel));
}
return (ctx, id) => {
for (let i = 0; i < compiled.length; i++) {
const fn = compiled[i];
if (fn(ctx, id)) {
return false;
}
}
return next(ctx, id);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchDescendant(next) {
return (ctx, id) => {
let current = ctx.getParent(id);
while (current > -1) {
if (next(ctx, current)) {
return true;
}
current = ctx.getParent(current);
}
return false;
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchChild(next) {
return (ctx, id) => {
const parent = ctx.getParent(id);
if (parent < 0) return false;
return next(ctx, parent);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchAdjacent(next) {
return (ctx, id) => {
const siblings = ctx.getSiblings(id);
const idx = siblings.indexOf(id) - 1;
if (idx < 0) return false;
const prev = siblings[idx];
return next(ctx, prev);
};
}
/**
* @param {NextFn} next
* @returns {MatcherFn}
*/
function matchFollowing(next) {
return (ctx, id) => {
const siblings = ctx.getSiblings(id);
const idx = siblings.indexOf(id) - 1;
if (idx < 0) return false;
for (let i = idx; i >= 0; i--) {
const sib = siblings[i];
if (next(ctx, sib)) return true;
}
return false;
};
}
/**
* @param {ElemSelector} part
* @param {MatcherFn} next
* @returns {MatcherFn}
*/
function matchElem(part, next) {
return (ctx, id) => {
if (part.wildcard) return next(ctx, id);
const type = ctx.getType(id);
if (type > -1 && type === part.elem) return next(ctx, id);
return false;
};
}
/**
* @param {AttrExists} attr
* @param {MatcherFn} next
* @returns {MatcherFn}
*/
function matchAttrExists(attr, next) {
return (ctx, id) => {
return ctx.hasAttrPath(id, attr.prop, 0) ? next(ctx, id) : false;
};
}
/**
* @param {AttrBin} attr
* @param {MatcherFn} next
* @returns {MatcherFn}
*/
function matchAttrBin(attr, next) {
return (ctx, id) => {
if (!ctx.hasAttrPath(id, attr.prop, 0)) return false;
const value = ctx.getAttrPathValue(id, attr.prop, 0);
if (!matchAttrValue(attr, value)) return false;
return next(ctx, id);
};
}
/**
* @param {AttrBin} attr
* @param {*} value
* @returns {boolean}
*/
function matchAttrValue(attr, value) {
switch (attr.op) {
case BinOp.Equal:
return value === attr.value;
case BinOp.NotEqual:
return value !== attr.value;
case BinOp.Greater:
return typeof value === "number" && typeof attr.value === "number" &&
value > attr.value;
case BinOp.GreaterThan:
return typeof value === "number" && typeof attr.value === "number" &&
value >= attr.value;
case BinOp.Less:
return typeof value === "number" && typeof attr.value === "number" &&
value < attr.value;
case BinOp.LessThan:
return typeof value === "number" && typeof attr.value === "number" &&
value <= attr.value;
default:
return false;
}
}

View file

@ -1,600 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import {
ATTR_BIN_NODE,
ATTR_EXISTS_NODE,
BinOp,
ELEM_NODE,
Lexer,
parseSelector,
PSEUDO_FIRST_CHILD,
PSEUDO_HAS,
PSEUDO_LAST_CHILD,
PSEUDO_NOT,
PSEUDO_NTH_CHILD,
RELATION_NODE,
Token,
} from "./40_lint_selector.js";
import { expect } from "@std/expect";
interface LexState {
token: number;
value: string;
}
function testLexer(input: string): LexState[] {
const out: LexState[] = [];
const l = new Lexer(input);
while (l.token !== Token.EOF) {
out.push({ token: l.token, value: l.value });
l.next();
}
return out;
}
const Tags: Record<string, number> = { Foo: 1, Bar: 2, FooBar: 3 };
const Attrs: Record<string, number> = { foo: 1, bar: 2, foobar: 3, attr: 4 };
const toTag = (name: string): number => Tags[name];
const toAttr = (name: string): number => Attrs[name];
const testParse = (input: string) => parseSelector(input, toTag, toAttr);
Deno.test("Lexer - Elem", () => {
expect(testLexer("Foo")).toEqual([
{ token: Token.Word, value: "Foo" },
]);
expect(testLexer("foo-bar")).toEqual([
{ token: Token.Word, value: "foo-bar" },
]);
expect(testLexer("foo_bar")).toEqual([
{ token: Token.Word, value: "foo_bar" },
]);
expect(testLexer("Foo Bar Baz")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Space, value: "" },
{ token: Token.Word, value: "Bar" },
{ token: Token.Space, value: "" },
{ token: Token.Word, value: "Baz" },
]);
expect(testLexer("Foo Bar Baz")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Space, value: "" },
{ token: Token.Word, value: "Bar" },
{ token: Token.Space, value: "" },
{ token: Token.Word, value: "Baz" },
]);
});
Deno.test("Lexer - Relation >", () => {
expect(testLexer("Foo > Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Op, value: ">" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer("Foo>Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Op, value: ">" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer(">Bar")).toEqual([
{ token: Token.Op, value: ">" },
{ token: Token.Word, value: "Bar" },
]);
});
Deno.test("Lexer - Relation +", () => {
expect(testLexer("Foo + Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Op, value: "+" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer("Foo+Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Op, value: "+" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer("+Bar")).toEqual([
{ token: Token.Op, value: "+" },
{ token: Token.Word, value: "Bar" },
]);
});
Deno.test("Lexer - Relation ~", () => {
expect(testLexer("Foo ~ Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Op, value: "~" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer("Foo~Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Op, value: "~" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer("~Bar")).toEqual([
{ token: Token.Op, value: "~" },
{ token: Token.Word, value: "Bar" },
]);
expect(testLexer("Foo Bar ~ Bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Space, value: "" },
{ token: Token.Word, value: "Bar" },
{ token: Token.Op, value: "~" },
{ token: Token.Word, value: "Bar" },
]);
});
Deno.test("Lexer - Attr", () => {
expect(testLexer("[attr]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr=1]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: "=" },
{ token: Token.Word, value: "1" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr='foo']")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: "=" },
{ token: Token.String, value: "foo" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr>=2]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: ">=" },
{ token: Token.Word, value: "2" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr<=2]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: "<=" },
{ token: Token.Word, value: "2" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr>2]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: ">" },
{ token: Token.Word, value: "2" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr<2]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: "<" },
{ token: Token.Word, value: "2" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr!=2]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Op, value: "!=" },
{ token: Token.Word, value: "2" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr.foo=1]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.Dot, value: "" },
{ token: Token.Word, value: "foo" },
{ token: Token.Op, value: "=" },
{ token: Token.Word, value: "1" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("[attr] [attr]")).toEqual([
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.BracketClose, value: "" },
{ token: Token.Space, value: "" },
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.BracketClose, value: "" },
]);
expect(testLexer("Foo[attr][attr2=1]")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr" },
{ token: Token.BracketClose, value: "" },
{ token: Token.BracketOpen, value: "" },
{ token: Token.Word, value: "attr2" },
{ token: Token.Op, value: "=" },
{ token: Token.Word, value: "1" },
{ token: Token.BracketClose, value: "" },
]);
});
Deno.test("Lexer - Pseudo", () => {
expect(testLexer(":foo-bar")).toEqual([
{ token: Token.Colon, value: "" },
{ token: Token.Word, value: "foo-bar" },
]);
expect(testLexer("Foo:foo-bar")).toEqual([
{ token: Token.Word, value: "Foo" },
{ token: Token.Colon, value: "" },
{ token: Token.Word, value: "foo-bar" },
]);
expect(testLexer(":foo-bar(baz)")).toEqual([
{ token: Token.Colon, value: "" },
{ token: Token.Word, value: "foo-bar" },
{ token: Token.BraceOpen, value: "" },
{ token: Token.Word, value: "baz" },
{ token: Token.BraceClose, value: "" },
]);
expect(testLexer(":foo-bar(2n + 1)")).toEqual([
{ token: Token.Colon, value: "" },
{ token: Token.Word, value: "foo-bar" },
{ token: Token.BraceOpen, value: "" },
{ token: Token.Word, value: "2n" },
{ token: Token.Op, value: "+" },
{ token: Token.Word, value: "1" },
{ token: Token.BraceClose, value: "" },
]);
});
Deno.test("Parser - Elem", () => {
expect(testParse("Foo")).toEqual([[
{
type: ELEM_NODE,
elem: 1,
wildcard: false,
},
]]);
});
Deno.test("Parser - Relation (descendant)", () => {
expect(testParse("Foo Bar")).toEqual([[
{
type: ELEM_NODE,
elem: 1,
wildcard: false,
},
{
type: RELATION_NODE,
op: BinOp.Space,
},
{
type: ELEM_NODE,
elem: 2,
wildcard: false,
},
]]);
});
Deno.test("Parser - Relation", () => {
expect(testParse("Foo > Bar")).toEqual([[
{
type: ELEM_NODE,
elem: 1,
wildcard: false,
},
{
type: RELATION_NODE,
op: BinOp.Greater,
},
{
type: ELEM_NODE,
elem: 2,
wildcard: false,
},
]]);
expect(testParse("Foo ~ Bar")).toEqual([[
{
type: ELEM_NODE,
elem: 1,
wildcard: false,
},
{
type: RELATION_NODE,
op: BinOp.Tilde,
},
{
type: ELEM_NODE,
elem: 2,
wildcard: false,
},
]]);
expect(testParse("Foo + Bar")).toEqual([[
{
type: ELEM_NODE,
elem: 1,
wildcard: false,
},
{
type: RELATION_NODE,
op: BinOp.Plus,
},
{
type: ELEM_NODE,
elem: 2,
wildcard: false,
},
]]);
});
Deno.test("Parser - Attr", () => {
expect(testParse("[foo]")).toEqual([[
{
type: ATTR_EXISTS_NODE,
prop: [1],
},
]]);
expect(testParse("[foo][bar]")).toEqual([[
{
type: ATTR_EXISTS_NODE,
prop: [1],
},
{
type: ATTR_EXISTS_NODE,
prop: [2],
},
]]);
expect(testParse("[foo=1]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: 1,
},
]]);
expect(testParse("[foo=true]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: true,
},
]]);
expect(testParse("[foo=false]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: false,
},
]]);
expect(testParse("[foo=null]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: null,
},
]]);
expect(testParse("[foo='str']")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: "str",
},
]]);
expect(testParse('[foo="str"]')).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: "str",
},
]]);
expect(testParse("[foo=/str/]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: /str/,
},
]]);
expect(testParse("[foo=/str/g]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1],
value: /str/g,
},
]]);
});
Deno.test("Parser - Attr nested", () => {
expect(testParse("[foo.bar]")).toEqual([[
{
type: ATTR_EXISTS_NODE,
prop: [1, 2],
},
]]);
expect(testParse("[foo.bar = 2]")).toEqual([[
{
type: ATTR_BIN_NODE,
op: BinOp.Equal,
prop: [1, 2],
value: 2,
},
]]);
});
Deno.test("Parser - Pseudo no value", () => {
expect(testParse(":first-child")).toEqual([[
{
type: PSEUDO_FIRST_CHILD,
},
]]);
expect(testParse(":last-child")).toEqual([[
{
type: PSEUDO_LAST_CHILD,
},
]]);
});
Deno.test("Parser - Pseudo nth-child", () => {
expect(testParse(":nth-child(2)")).toEqual([[
{
type: PSEUDO_NTH_CHILD,
of: null,
op: null,
step: 0,
stepOffset: 1,
repeat: false,
},
]]);
expect(testParse(":nth-child(2n)")).toEqual([[
{
type: PSEUDO_NTH_CHILD,
of: null,
op: null,
step: 2,
stepOffset: 0,
repeat: true,
},
]]);
expect(testParse(":nth-child(-2n)")).toEqual([[
{
type: PSEUDO_NTH_CHILD,
of: null,
op: null,
step: -2,
stepOffset: 0,
repeat: true,
},
]]);
expect(testParse(":nth-child(2n + 1)")).toEqual([[
{
type: PSEUDO_NTH_CHILD,
of: null,
op: "+",
step: 2,
stepOffset: 1,
repeat: true,
},
]]);
expect(testParse(":nth-child(2n + 1 of Foo[attr])"))
.toEqual([[
{
type: PSEUDO_NTH_CHILD,
of: [
{ type: ELEM_NODE, elem: 1, wildcard: false },
{ type: ATTR_EXISTS_NODE, prop: [4] },
],
op: "+",
step: 2,
stepOffset: 1,
repeat: true,
},
]]);
// Invalid selectors
expect(() => testParse(":nth-child(2n + 1 of Foo[attr], Bar)"))
.toThrow();
expect(() => testParse(":nth-child(2n - 1 foo)")).toThrow();
});
Deno.test("Parser - Pseudo has/is/where", () => {
expect(testParse(":has(Foo:has(Foo), Bar)")).toEqual([[
{
type: PSEUDO_HAS,
selectors: [
[
{ type: ELEM_NODE, elem: 1, wildcard: false },
{
type: PSEUDO_HAS,
selectors: [
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
],
},
],
[
{ type: ELEM_NODE, elem: 2, wildcard: false },
],
],
},
]]);
expect(testParse(":where(Foo:where(Foo), Bar)")).toEqual([[
{
type: PSEUDO_HAS,
selectors: [
[
{ type: ELEM_NODE, elem: 1, wildcard: false },
{
type: PSEUDO_HAS,
selectors: [
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
],
},
],
[
{ type: ELEM_NODE, elem: 2, wildcard: false },
],
],
},
]]);
expect(testParse(":is(Foo:is(Foo), Bar)")).toEqual([[
{
type: PSEUDO_HAS,
selectors: [
[
{ type: ELEM_NODE, elem: 1, wildcard: false },
{
type: PSEUDO_HAS,
selectors: [
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
],
},
],
[
{ type: ELEM_NODE, elem: 2, wildcard: false },
],
],
},
]]);
});
Deno.test("Parser - Pseudo not", () => {
expect(testParse(":not(Foo:not(Foo), Bar)")).toEqual([[
{
type: PSEUDO_NOT,
selectors: [
[
{ type: ELEM_NODE, elem: 1, wildcard: false },
{
type: PSEUDO_NOT,
selectors: [
[{ type: ELEM_NODE, elem: 1, wildcard: false }],
],
},
],
[
{ type: ELEM_NODE, elem: 2, wildcard: false },
],
],
},
]]);
});
Deno.test("Parser - mixed", () => {
expect(testParse("Foo[foo=true] Bar")).toEqual([[
{
type: ELEM_NODE,
elem: 1,
wildcard: false,
},
{ type: ATTR_BIN_NODE, op: BinOp.Equal, prop: [1], value: true },
{ type: RELATION_NODE, op: BinOp.Space },
{
type: ELEM_NODE,
elem: 2,
wildcard: false,
},
]]);
});

View file

@ -1,477 +0,0 @@
import { splitSelectors } from "./40_lint_selector.js";
import {
compileSelector,
MatchCtx,
MatcherFn,
parseSelector,
} from "./40_lint_selector.js";
import { expect } from "@std/expect";
/**
* TS eslint selector Examples
*
* ```js
* ForOfStatement[await=true]
* VariableDeclaration[kind="await using"]
* MethodDefinition[kind="constructor"] ThisExpression
* PropertyDefinition > ArrowFunctionExpression.value
* PropertyDefinition > *.key
* ThisExpression, Super
* VariableDeclarator,PropertyDefinition,:matches(FunctionDeclaration,FunctionExpression) > AssignmentPattern
* ImportDeclaration[importKind = "type"]
* ImportSpecifier[importKind = "type"]
* ExportNamedDeclaration:not([source])
* UnaryExpression[operator="delete"]
* AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]
* CallExpression > MemberExpression.callee > Identifier[name = "join"].property
* CallExpression > MemberExpression.callee > Identifier[name = /^(toLocaleString|toString)$/].property
* ImportDeclaration[importKind!="type"]
* UnaryExpression[operator="void"]
* UnaryExpression[operator="!"]
* LogicalExpression[operator = "??"] > TSNonNullExpression.left
* CallExpression[callee.name="require"]
* :not(ArrowFunctionExpression) > TSTypeParameterDeclaration > TSTypeParameter[constraint]
* ArrowFunctionExpression > TSTypeParameterDeclaration > TSTypeParameter[constraint]
* PropertyDefinition[value != null]
* :not(ObjectPattern) > Property
* CallExpression > *.callee
* TaggedTemplateExpression > *.tag
* BinaryExpression[operator=/^[<>!=]?={0,2}$/]
* :matches(ClassDeclaration, ClassExpression)
* MethodDefinition[kind="constructor"]
* MemberExpression[computed=true]
* TSTypeLiteral[members.length = 1]
* CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]
* CallExpression > MemberExpression.callee
* CallExpression[arguments.length=1] > MemberExpression
* CallExpression > MemberExpression.callee[property.name="test"][computed=false]
* :matches(MethodDefinition, TSMethodSignature)[kind=get]
* ArrowFunctionExpression[async = true] > :not(BlockStatement, AwaitExpression)
* ```
*/
const AstNodes: Record<string, number> = {
Foo: 1,
Bar: 2,
Baz: 3,
Foobar: 4,
};
const AstAttrs: Record<string, number> = {
_empty: 0,
key: 1,
value: 2,
attr: 3,
attr2: 4,
children: 5,
msg: 6,
};
const toElem = (value: string) => AstNodes[value];
const toAttr = (value: string) => AstAttrs[value];
export interface TestNode {
type: keyof typeof AstNodes;
children?: TestNode[];
[key: string]: number | boolean | string | TestNode | TestNode[] | undefined;
}
export interface FakeProp {
propId: number;
name: string;
value: unknown;
}
export interface FakeNode {
type: number;
name: string;
parentId: number;
props: FakeProp[];
original: TestNode;
}
function isFakeNode(x: unknown): x is FakeNode {
return x !== null && typeof x === "object" && "type" in x &&
typeof x.type === "number" &&
"original" in x;
}
class FakeContext implements MatchCtx {
ids = new Map<number, FakeNode>();
idByNode = new Map<FakeNode, number>();
id = 0;
getAttrPathValue(id: number, props: number[]) {
const node = this.ids.get(id);
if (node === undefined) return undefined;
// deno-lint-ignore no-explicit-any
let tmp: any = node;
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (isFakeNode(tmp)) {
const found = tmp.props.find((node) => node.propId === prop);
if (!found) return undefined;
tmp = found.value;
continue;
}
const name = AstAttrs[prop];
if (!(name in tmp)) return undefined;
tmp = tmp[name];
}
return tmp;
}
hasAttrPath(id: number, propId: number[]): boolean {
return this.getAttrPathValue(id, propId) !== undefined;
}
getType(id: number): number {
const node = this.ids.get(id);
if (node === undefined) return -1;
return node.type;
}
getFirstChild(id: number): number {
const node = this.ids.get(id);
if (node === undefined) return -1;
let first = -1;
// First check if there is an array prop
for (const prop of node.props) {
if (Array.isArray(prop.value)) {
if (prop.value.length === 0) return -1;
return prop.value[0];
} else if (
first === -1 && prop.value !== null && typeof prop.value === "object"
) {
// @ts-ignore loosely typed
first = prop.value;
}
}
return first;
}
getLastChild(id: number): number {
const node = this.ids.get(id);
if (node === undefined) return -1;
let last = -1;
// First check if there is an array prop
for (const prop of node.props) {
if (Array.isArray(prop.value)) {
if (prop.value.length === 0) return -1;
return prop.value.at(-1);
} else if (prop.value !== null && typeof prop.value !== "object") {
// @ts-ignore loosely typed
last = prop.value;
}
}
return last;
}
getParent(id: number): number {
const node = this.ids.get(id);
if (node === undefined) return -1;
return node.parentId;
}
getSiblings(id: number): number[] {
const parent = this.getParent(id);
const node = this.ids.get(parent);
if (node === undefined) return [];
for (const prop of node.props) {
if (Array.isArray(prop.value)) {
if (prop.value.includes(id)) {
return prop.value;
}
}
}
return [];
}
}
function fakeSerializeAst(node: TestNode): FakeContext {
const ctx = new FakeContext();
serializeFakeNode(ctx, node, -1);
return ctx;
}
function serializeFakeNode(
ctx: FakeContext,
node: TestNode,
parentId: number,
): number {
const id = ctx.id;
ctx.id++;
const type = AstNodes[node.type];
const props: FakeProp[] = [];
const fake: FakeNode = {
type,
name: node.type,
parentId,
props,
get original() {
return node;
},
};
ctx.ids.set(id, fake);
ctx.idByNode.set(fake, id);
for (const [k, value] of Object.entries(node)) {
if (k === "type") continue;
// deno-lint-ignore no-explicit-any
const propId = (AstAttrs as any)[k] as number;
const prop: FakeProp = {
propId: propId,
name: k,
value: null,
};
props.push(prop);
if (value !== null && typeof value === "object") {
if (Array.isArray(value)) {
prop.value = value.map((v) => serializeFakeNode(ctx, v, id));
} else {
prop.value = serializeFakeNode(ctx, value, id);
}
} else {
prop.value = value;
}
}
return id;
}
function visit(
ctx: FakeContext,
selector: MatcherFn,
id: number,
): unknown {
const node = ctx.ids.get(id)!;
// console.log("visit", { node });
const res = selector(ctx, id);
if (res) {
// console.log("<-- MATCHED");
return node.original;
}
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i];
const value = prop.value;
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const res = visit(ctx, selector, value[i]);
if (res) {
return res;
}
}
} else if (isFakeNode(value)) {
const id = ctx.idByNode.get(value)!;
const res = visit(ctx, selector, id);
if (res) {
return res;
}
}
}
}
function testSelector(
ast: TestNode,
selector: string,
): unknown {
const ctx = fakeSerializeAst(ast);
const raw = parseSelector(selector, toElem, toAttr)[0];
const sel = compileSelector(raw);
return visit(ctx, sel, 0);
}
Deno.test("select descendant: A B", () => {
const ast: TestNode = {
type: "Foo",
children: [{ type: "Bar" }, { type: "Baz" }],
};
expect(testSelector(ast, "Foo")).toEqual(ast);
expect(testSelector(ast, "Foo Bar")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo Baz")).toEqual(ast.children![1]);
// Not matching
expect(testSelector(ast, "Foo Foo")).toEqual(undefined);
});
Deno.test("select child: A > B", () => {
const ast: TestNode = {
type: "Foo",
foo: "fail",
children: [{ type: "Bar", children: [{ type: "Foo" }] }, {
type: "Foo",
}],
};
expect(testSelector(ast, "Foo > Foo")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo>Foo")).toEqual(ast.children![1]);
expect(testSelector(ast, "* > Foo")).toEqual(ast.children![1]);
expect(testSelector(ast, "*> Foo")).toEqual(ast.children![1]);
expect(testSelector(ast, "* > *> Foo")).toEqual(ast.children![1]);
});
Deno.test("select child: A > B #2", () => {
const ast: TestNode = {
type: "Foo",
foo: "fail",
children: [{ type: "Bar", children: [{ type: "Foo" }] }],
};
expect(testSelector(ast, "Foo > Foo")).toEqual(undefined);
expect(testSelector(ast, "Foo>Foo")).toEqual(undefined);
});
Deno.test("select child: A + B", () => {
const ast: TestNode = {
type: "Foo",
children: [
{ type: "Bar", msg: "FAIL" },
{ type: "Bar", msg: "FAIL" },
{ type: "Baz" },
{ type: "Baz", msg: "FAIL" },
{ type: "Foo", msg: "FAIL" },
{ type: "Baz", msg: "FAIL" },
],
};
expect(testSelector(ast, "Bar + Baz")).toEqual(ast.children![2]);
expect(testSelector(ast, "Bar+Baz")).toEqual(ast.children![2]);
});
Deno.test("select child: A ~ B", () => {
const ast: TestNode = {
type: "Foo",
children: [
{ type: "Bar", msg: "FAIL" },
{ type: "Bar", msg: "FAIL" },
{ type: "Foo", msg: "FAIL" },
{ type: "Baz", msg: "ok #1" },
],
};
expect(testSelector(ast, "Bar ~ Baz")).toEqual(ast.children![3]);
expect(testSelector(ast, "Bar~Baz")).toEqual(ast.children![3]);
});
Deno.test("select child: A[attr]", () => {
const ast: TestNode = {
type: "Foo",
children: [
{ type: "Foo", msg: "a" },
{ type: "Bar", msg: "b" },
{ type: "Baz" },
],
};
expect(testSelector(ast, "[msg]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg=a]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg = a]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg='a']")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg = 'a']")).toEqual(ast.children![0]);
expect(testSelector(ast, 'Foo[msg="a"]')).toEqual(ast.children![0]);
expect(testSelector(ast, 'Foo[msg = "a"]')).toEqual(ast.children![0]);
});
Deno.test("select child: A[attr <op> value]", () => {
const ast: TestNode = {
type: "Foo",
children: [
{ type: "Foo", msg: false },
{ type: "Foo", msg: true },
{ type: "Foo", msg: 1 },
{ type: "Foo", msg: 2 },
],
};
expect(testSelector(ast, "Foo[msg=true]")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo[msg = true]")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo[msg=false]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg = false]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg=1]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg = 1]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg!=true]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg != true]")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo[msg!=false]")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo[msg != false]")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo[msg<2]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg < 2]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg<=1]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg <= 1]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg>1]")).toEqual(ast.children![3]);
expect(testSelector(ast, "Foo[msg > 1]")).toEqual(ast.children![3]);
expect(testSelector(ast, "Foo[msg>=1]")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo[msg >= 1]")).toEqual(ast.children![2]);
});
Deno.test("select child: A:first-child", () => {
const ast: TestNode = {
type: "Foo",
children: [
{ type: "Foo", msg: "a" },
{ type: "Foo", msg: "b" },
{ type: "Foo", msg: "c" },
{ type: "Foo", msg: "d" },
],
};
expect(testSelector(ast, "Foo:first-child")).toEqual(ast.children![0]);
expect(testSelector(ast, "Foo:last-child")).toEqual(ast.children!.at(-1));
});
Deno.test("select child: A:nth-child", () => {
const ast: TestNode = {
type: "Bar",
children: [
{ type: "Foo", msg: "a" },
{ type: "Foo", msg: "b" },
{ type: "Foo", msg: "c" },
{ type: "Foo", msg: "d" },
],
};
expect(testSelector(ast, "Foo:nth-child(2)")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo:nth-child(2n)")).toEqual(ast.children![1]);
expect(testSelector(ast, "Foo:nth-child(2n + 3)")).toEqual(ast.children![2]);
expect(testSelector(ast, "Foo:nth-child(2n - 1)")).toEqual(ast.children![0]);
expect(testSelector(ast, ":nth-child(2n + 2 of Foo)")).toEqual(
ast.children![1],
);
});
Deno.test("splitSelectors", () => {
expect(splitSelectors("foo")).toEqual(["foo"]);
expect(splitSelectors("foo, bar")).toEqual(["foo", "bar"]);
expect(splitSelectors("foo:f(bar, baz)")).toEqual(["foo:f(bar, baz)"]);
expect(splitSelectors("foo:f(bar, baz), foobar")).toEqual([
"foo:f(bar, baz)",
"foobar",
]);
});
Deno.test("mixed", () => {
const ast: TestNode = {
type: "Foo",
attr: true,
children: [
{ type: "Foo", msg: "a" },
{ type: "Foo", msg: "b" },
{ type: "Foo", msg: "c" },
{ type: "Foo", msg: "d" },
],
};
expect(testSelector(ast, "Foo[attr=true] Foo")).toEqual(ast.children![0]);
});

View file

@ -116,7 +116,7 @@ export type TransformFn = (value: string) => number;
export type VisitorFn = (node: Deno.AstNode) => void;
export interface CompiledVisitor {
matcher: MatcherFn;
matcher: (offset: number) => boolean;
info: { enter: VisitorFn; exit: VisitorFn };
}

View file

@ -658,7 +658,6 @@ impl CliMainWorkerFactory {
"40_bench.js",
"40_jupyter.js",
// TODO(bartlomieju): probably shouldn't include these files here?
"40_lint_selector.js",
"40_lint.js"
);
}