diff --git a/cli/js/40_lint.js b/cli/js/40_lint.js index 48290c6d3a..dbf18489e0 100644 --- a/cli/js/40_lint.js +++ b/cli/js/40_lint.js @@ -3,9 +3,11 @@ // @ts-check import { core } from "ext:core/mod.js"; -import { compileSelector, parseSelector } from "ext:cli/lint/selector.js"; - -console.log({ compileSelector, foo: "foo" }); +import { + compileSelector, + parseSelector, + splitSelectors, +} from "ext:cli/lint/selector.js"; const { op_lint_get_rule, @@ -37,8 +39,9 @@ const PropFlags = { * rootId: number, * nodes: Map, * strByType: number[], - * typeByStr: Map, * strByProp: number[] + * typeByStr: Map, + * propByStr: Map, * }} AstContext */ @@ -49,6 +52,19 @@ const PropFlags = { * }} LintState */ +/** + * @typedef {import("./40_lint_types.d.ts").VisitorFn} VisitorFn + */ +/** + * @typedef {import("./40_lint_types.d.ts").MatcherFn} MatcherFn + */ +/** + * @typedef {import("./40_lint_types.d.ts").TransformFn} TransformerFn + */ +/** + * @typedef {import("./40_lint_types.d.ts").CompiledVisitor} CompiledVisitor + */ + /** @type {LintState} */ const state = { plugins: [], @@ -418,12 +434,14 @@ function createAstContext(buf) { const propCount = readU32(buf, offset); offset += 4; + const propByStr = new Map(); const strByProp = new Array(propCount).fill(0); for (let i = 0; i < propCount; i++) { const v = readU32(buf, offset); offset += 4; strByProp[i] = v; + propByStr.set(strTable.get(v), i); } /** @type {AstContext} */ @@ -436,6 +454,7 @@ function createAstContext(buf) { strByProp, strByType, typeByStr, + propByStr, }; setNodeGetters(ctx); @@ -445,6 +464,11 @@ function createAstContext(buf) { return ctx; } +/** + * @param {*} _node + */ +const NOOP = (_node) => {}; + /** * @param {string} fileName * @param {Uint8Array} serializedAst @@ -453,8 +477,9 @@ export function runPluginsForFile(fileName, serializedAst) { const ctx = createAstContext(serializedAst); // console.log(JSON.stringify(ctx, null, 2)); - /** @type {Record void>} */ - const mergedVisitor = {}; + /** @type {Map} */ + const bySelector = new Map(); + const destroyFns = []; // console.log(state); @@ -472,23 +497,48 @@ export function runPluginsForFile(fileName, serializedAst) { // console.log({ visitor }); - for (const name in visitor) { - const prev = mergedVisitor[name]; - mergedVisitor[name] = (node) => { - if (typeof prev === "function") { - prev(node); - } + for (let key in visitor) { + const fn = visitor[key]; - try { - visitor[name](node); - } catch (err) { - // FIXME: console here doesn't support error cause - console.log(err); - throw new Error(`Visitor "${name}" of plugin "${id}" errored`, { - cause: err, - }); + let isExit = false; + if (key.endsWith(":exit")) { + 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); } - }; + const prevFn = isExit ? info.exit : info.enter; + + /** + * @param {*} node + */ + const wrapped = (node) => { + prevFn(node); + + try { + fn(node); + } catch (err) { + // FIXME: console here doesn't support error cause + console.log(err); + throw new Error(`Visitor "${name}" of plugin "${id}" errored`, { + cause: err, + }); + } + }; + + if (isExit) { + info.exit = wrapped; + } else { + info.enter = wrapped; + } + } } if (typeof rule.destroy === "function") { @@ -504,10 +554,36 @@ 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.typeByStr.get(str); + if (id === undefined) throw new Error(`Unknown elem: ${str}`); + return id; + }; + + /** @type {CompiledVisitor[]} */ + const visitors = []; + for (const [sel, info] of bySelector.entries()) { + const compiled = parseSelector(sel, toElem, toAttr); + const matcher = compileSelector(compiled); + + visitors.push({ + info, + matcher, + }); + } + // Traverse ast with all visitors at the same time to avoid traversing // multiple times. try { - traverse(ctx, mergedVisitor); + traverse(ctx, visitors, ctx.rootId); } finally { ctx.nodes.clear(); @@ -520,76 +596,78 @@ export function runPluginsForFile(fileName, serializedAst) { /** * @param {AstContext} ctx - * @param {*} visitor - * @returns {void} - */ -function traverse(ctx, visitor) { - const visitTypes = new Map(); - - // TODO: create visiting types - for (const name in visitor) { - const id = ctx.typeByStr.get(name); - if (id === undefined) continue; - visitTypes.set(id, name); - } - - console.log("merged visitor", visitor); - console.log("visiting types", visitTypes); - - traverseInner(ctx, visitTypes, visitor, ctx.rootId); -} - -/** - * @param {AstContext} ctx - * @param {Map} visitTypes - * @param {Record void>} visitor + * @param {CompiledVisitor[]} visitors * @param {number} offset */ -function traverseInner(ctx, visitTypes, visitor, offset) { +function traverse(ctx, visitors, offset) { // console.log("traversing offset", offset); // Empty id if (offset === 0) return; const { buf } = ctx; - const type = buf[offset]; - const name = visitTypes.get(type); - if (name !== undefined) { - // console.log("--> invoking visitor"); - const node = new Node(ctx, offset); - visitor[name](node); + /** @type {VisitorFn[] | null} */ + let exits = null; + + for (let i = 0; i < visitors.length; i++) { + const v = visitors[i]; + + if (v.info.enter === NOOP) { + continue; + } + + // FIXME: add matcher context methods + if (v.matcher(ctx, offset)) { + const node = /** @type {*} */ (getNode(ctx, offset)); + v.info.enter(node); + + if (exits === null) { + exits = [v.info.exit]; + } else { + exits.push(v.info.exit); + } + } } - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; + try { + // type + parentId + SpanLo + SpanHi + offset += 1 + 4 + 4 + 4; - const propCount = buf[offset]; - offset += 1; - // console.log({ propCount }); + const propCount = buf[offset]; + offset += 1; + // console.log({ propCount }); - for (let i = 0; i < propCount; i++) { - const kind = buf[offset + 1]; - offset += 2; // propId + propFlags + for (let i = 0; i < propCount; i++) { + const kind = buf[offset + 1]; + offset += 2; // propId + propFlags - if (kind === PropFlags.Ref) { - const next = readU32(buf, offset); - offset += 4; - traverseInner(ctx, visitTypes, visitor, next); - } else if (kind === PropFlags.RefArr) { - const len = readU32(buf, offset); - offset += 4; - - for (let j = 0; j < len; j++) { - const chiild = readU32(buf, offset); + if (kind === PropFlags.Ref) { + const next = readU32(buf, offset); offset += 4; - traverseInner(ctx, visitTypes, visitor, chiild); + traverse(ctx, visitors, next); + } else if (kind === PropFlags.RefArr) { + const len = readU32(buf, offset); + offset += 4; + + for (let j = 0; j < len; j++) { + const child = readU32(buf, offset); + offset += 4; + traverse(ctx, visitors, child); + } + } else if (kind === PropFlags.String) { + offset += 4; + } else if (kind === PropFlags.Bool) { + offset += 1; + } else if (kind === PropFlags.Null || kind === PropFlags.Undefined) { + // No value + } + } + } finally { + if (exits !== null) { + for (let i = 0; i < exits.length; i++) { + const node = /** @type {*} */ (getNode(ctx, offset)); + exits[i](node); } - } else if (kind === PropFlags.String) { - offset += 4; - } else if (kind === PropFlags.Bool) { - offset += 1; - } else if (kind === PropFlags.Null || kind === PropFlags.Undefined) { - // No value } } } diff --git a/cli/js/40_lint_selector.js b/cli/js/40_lint_selector.js index 4a67066d2c..e9106ca5b8 100644 --- a/cli/js/40_lint_selector.js +++ b/cli/js/40_lint_selector.js @@ -17,7 +17,7 @@ /** @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").Transformer} Transformer */ +/** @typedef {import("./40_lint_types.d.ts").TransformFn} Transformer */ const Char = { Tab: 9, @@ -377,6 +377,42 @@ 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 diff --git a/cli/js/40_lint_selector_test.ts b/cli/js/40_lint_selector_test.ts index e2b35578ed..976ddee3fa 100644 --- a/cli/js/40_lint_selector_test.ts +++ b/cli/js/40_lint_selector_test.ts @@ -1,3 +1,4 @@ +import { splitSelectors } from "./40_lint_selector.js"; import { compileSelector, MatchCtx, @@ -469,3 +470,13 @@ Deno.test("select child: A:nth-child", () => { 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", + ]); +}); diff --git a/cli/js/40_lint_types.d.ts b/cli/js/40_lint_types.d.ts index c9c0d683b2..03d0375617 100644 --- a/cli/js/40_lint_types.d.ts +++ b/cli/js/40_lint_types.d.ts @@ -102,6 +102,12 @@ export interface MatchCtx { export type NextFn = (ctx: MatchCtx, id: number) => boolean; export type MatcherFn = (ctx: MatchCtx, id: number) => boolean; -export type Transformer = (value: string) => number; +export type TransformFn = (value: string) => number; +export type VisitorFn = (node: Deno.AstNode) => void; + +export interface CompiledVisitor { + matcher: MatcherFn; + info: { enter: VisitorFn; exit: VisitorFn }; +} export {};