mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 04:52:26 -05:00
feat(unstable): support selectors in JS lint plugins (#27452)
This PR adds support for using selectors in the JS linting plugin API. Supported at the moment are: - `Foo Bar` (descendant) - `Foo > Bar` (child combinator) - `Foo + Foo` (next sibling) - `Foo ~ Foo` (subsequent sibling) - `[attr]`, `[attr=value]` (attribute selectors, supported operators: `=`, `!=`, `<`, `>`, `<=`, `>=`) - `:first-child` - `:last-child` - `:nth-child(2)`, `:nth-child(2n + 1)`
This commit is contained in:
parent
3cc861cdca
commit
1a809b8115
10 changed files with 2272 additions and 54 deletions
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import {
|
||||||
|
compileSelector,
|
||||||
|
parseSelector,
|
||||||
|
splitSelectors,
|
||||||
|
} from "ext:cli/40_lint_selector.js";
|
||||||
import { core, internals } from "ext:core/mod.js";
|
import { core, internals } from "ext:core/mod.js";
|
||||||
const {
|
const {
|
||||||
op_lint_create_serialized_ast,
|
op_lint_create_serialized_ast,
|
||||||
|
@ -13,6 +18,7 @@ const {
|
||||||
const AST_PROP_TYPE = 0;
|
const AST_PROP_TYPE = 0;
|
||||||
const AST_PROP_PARENT = 1;
|
const AST_PROP_PARENT = 1;
|
||||||
const AST_PROP_RANGE = 2;
|
const AST_PROP_RANGE = 2;
|
||||||
|
const AST_PROP_LENGTH = 3;
|
||||||
|
|
||||||
// Keep in sync with Rust
|
// Keep in sync with Rust
|
||||||
// Each node property is tagged with this enum to denote
|
// Each node property is tagged with this enum to denote
|
||||||
|
@ -43,8 +49,8 @@ const PropFlags = {
|
||||||
/** @typedef {import("./40_lint_types.d.ts").RuleContext} RuleContext */
|
/** @typedef {import("./40_lint_types.d.ts").RuleContext} RuleContext */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").NodeFacade} NodeFacade */
|
/** @typedef {import("./40_lint_types.d.ts").NodeFacade} NodeFacade */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").LintPlugin} LintPlugin */
|
/** @typedef {import("./40_lint_types.d.ts").LintPlugin} LintPlugin */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").LintReportData} LintReportData */
|
/** @typedef {import("./40_lint_types.d.ts").TransformFn} TransformFn */
|
||||||
/** @typedef {import("./40_lint_types.d.ts").TestReportData} TestReportData */
|
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchContext */
|
||||||
|
|
||||||
/** @type {LintState} */
|
/** @type {LintState} */
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -99,7 +105,6 @@ export function installPlugin(plugin) {
|
||||||
*/
|
*/
|
||||||
function getNode(ctx, offset) {
|
function getNode(ctx, offset) {
|
||||||
if (offset === 0) return null;
|
if (offset === 0) return null;
|
||||||
|
|
||||||
const cached = ctx.nodes.get(offset);
|
const cached = ctx.nodes.get(offset);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
|
@ -297,9 +302,10 @@ function readValue(ctx, offset, search) {
|
||||||
if (offset === -1) return undefined;
|
if (offset === -1) return undefined;
|
||||||
|
|
||||||
const kind = buf[offset + 1];
|
const kind = buf[offset + 1];
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
if (kind === PropFlags.Ref) {
|
if (kind === PropFlags.Ref) {
|
||||||
const value = readU32(buf, offset + 2);
|
const value = readU32(buf, offset);
|
||||||
return getNode(ctx, value);
|
return getNode(ctx, value);
|
||||||
} else if (kind === PropFlags.RefArr) {
|
} else if (kind === PropFlags.RefArr) {
|
||||||
const len = readU32(buf, offset);
|
const len = readU32(buf, offset);
|
||||||
|
@ -353,6 +359,303 @@ function getString(strTable, id) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AstContext["buf"]} buf
|
||||||
|
* @param {number} child
|
||||||
|
* @returns {null | [number, number]}
|
||||||
|
*/
|
||||||
|
function findChildOffset(buf, child) {
|
||||||
|
let offset = readU32(buf, child + 1);
|
||||||
|
|
||||||
|
// type + parentId + SpanLo + SpanHi
|
||||||
|
offset += 1 + 4 + 4 + 4;
|
||||||
|
|
||||||
|
const propCount = buf[offset++];
|
||||||
|
for (let i = 0; i < propCount; i++) {
|
||||||
|
const _prop = buf[offset++];
|
||||||
|
const kind = buf[offset++];
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case PropFlags.Ref: {
|
||||||
|
const start = offset;
|
||||||
|
const value = readU32(buf, offset);
|
||||||
|
offset += 4;
|
||||||
|
if (value === child) {
|
||||||
|
return [start, -1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PropFlags.RefArr: {
|
||||||
|
const start = offset;
|
||||||
|
|
||||||
|
const len = readU32(buf, offset);
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
const value = readU32(buf, offset);
|
||||||
|
offset += 4;
|
||||||
|
if (value === child) {
|
||||||
|
return [start, j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PropFlags.String:
|
||||||
|
offset += 4;
|
||||||
|
break;
|
||||||
|
case PropFlags.Bool:
|
||||||
|
offset++;
|
||||||
|
break;
|
||||||
|
case PropFlags.Null:
|
||||||
|
case PropFlags.Undefined:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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) {
|
||||||
|
const count = readU32(buf, offset);
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
if (idx < propIds.length - 1 && propIds[idx + 1] === AST_PROP_LENGTH) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(@marvinhagemeister): Allow traversing into array children?
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (idx < propIds.length - 1 && propIds[idx + 1] === AST_PROP_LENGTH) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(@marvinhagemeister): Allow traversing into array children?
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {Uint8Array} buf
|
||||||
* @param {AstContext} buf
|
* @param {AstContext} buf
|
||||||
|
@ -433,6 +736,7 @@ function createAstContext(buf) {
|
||||||
strByType,
|
strByType,
|
||||||
typeByStr,
|
typeByStr,
|
||||||
propByStr,
|
propByStr,
|
||||||
|
matcher: new MatchCtx(buf, strTable),
|
||||||
};
|
};
|
||||||
|
|
||||||
setNodeGetters(ctx);
|
setNodeGetters(ctx);
|
||||||
|
@ -456,7 +760,7 @@ const NOOP = (_node) => {};
|
||||||
export function runPluginsForFile(fileName, serializedAst) {
|
export function runPluginsForFile(fileName, serializedAst) {
|
||||||
const ctx = createAstContext(serializedAst);
|
const ctx = createAstContext(serializedAst);
|
||||||
|
|
||||||
/** @type {Map<string, { enter: VisitorFn, exit: VisitorFn}>} */
|
/** @type {Map<string, CompiledVisitor["info"]>}>} */
|
||||||
const bySelector = new Map();
|
const bySelector = new Map();
|
||||||
|
|
||||||
const destroyFns = [];
|
const destroyFns = [];
|
||||||
|
@ -486,32 +790,38 @@ export function runPluginsForFile(fileName, serializedAst) {
|
||||||
key = key.slice(0, -":exit".length);
|
key = key.slice(0, -":exit".length);
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = bySelector.get(key);
|
const selectors = splitSelectors(key);
|
||||||
if (info === undefined) {
|
|
||||||
info = { enter: NOOP, exit: NOOP };
|
|
||||||
bySelector.set(key, info);
|
|
||||||
}
|
|
||||||
const prevFn = isExit ? info.exit : info.enter;
|
|
||||||
|
|
||||||
/**
|
for (let j = 0; j < selectors.length; j++) {
|
||||||
* @param {*} node
|
const key = selectors[j];
|
||||||
*/
|
|
||||||
const wrapped = (node) => {
|
|
||||||
prevFn(node);
|
|
||||||
|
|
||||||
try {
|
let info = bySelector.get(key);
|
||||||
fn(node);
|
if (info === undefined) {
|
||||||
} catch (err) {
|
info = { enter: NOOP, exit: NOOP };
|
||||||
throw new Error(`Visitor "${name}" of plugin "${id}" errored`, {
|
bySelector.set(key, info);
|
||||||
cause: err,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
const prevFn = isExit ? info.exit : info.enter;
|
||||||
|
|
||||||
if (isExit) {
|
/**
|
||||||
info.exit = wrapped;
|
* @param {*} node
|
||||||
} else {
|
*/
|
||||||
info.enter = wrapped;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,25 +838,27 @@ export function runPluginsForFile(fileName, serializedAst) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create selectors
|
||||||
|
/** @type {TransformFn} */
|
||||||
|
const toElem = (str) => {
|
||||||
|
const id = ctx.typeByStr.get(str);
|
||||||
|
return id === undefined ? 0 : id;
|
||||||
|
};
|
||||||
|
/** @type {TransformFn} */
|
||||||
|
const toAttr = (str) => {
|
||||||
|
const id = ctx.propByStr.get(str);
|
||||||
|
return id === undefined ? 0 : id;
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {CompiledVisitor[]} */
|
/** @type {CompiledVisitor[]} */
|
||||||
const visitors = [];
|
const visitors = [];
|
||||||
for (const [sel, info] of bySelector.entries()) {
|
for (const [sel, info] of bySelector.entries()) {
|
||||||
// This will make more sense once selectors land as it's faster
|
// Selectors are already split here.
|
||||||
// to precompile them once upfront.
|
// TODO(@marvinhagemeister): Avoid array allocation (not sure if that matters)
|
||||||
|
const parsed = parseSelector(sel, toElem, toAttr)[0];
|
||||||
|
const matcher = compileSelector(parsed);
|
||||||
|
|
||||||
// Convert the visiting element name to a number. This number
|
visitors.push({ info, matcher });
|
||||||
// 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,
|
|
||||||
// Check if we should call this visitor
|
|
||||||
matcher: (offset) => {
|
|
||||||
const type = ctx.buf[offset];
|
|
||||||
return type === elemId;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traverse ast with all visitors at the same time to avoid traversing
|
// Traverse ast with all visitors at the same time to avoid traversing
|
||||||
|
@ -572,6 +884,8 @@ function traverse(ctx, visitors, offset) {
|
||||||
// The 0 offset is used to denote an empty/placeholder node
|
// The 0 offset is used to denote an empty/placeholder node
|
||||||
if (offset === 0) return;
|
if (offset === 0) return;
|
||||||
|
|
||||||
|
const originalOffset = offset;
|
||||||
|
|
||||||
const { buf } = ctx;
|
const { buf } = ctx;
|
||||||
|
|
||||||
/** @type {VisitorFn[] | null} */
|
/** @type {VisitorFn[] | null} */
|
||||||
|
@ -580,7 +894,7 @@ function traverse(ctx, visitors, offset) {
|
||||||
for (let i = 0; i < visitors.length; i++) {
|
for (let i = 0; i < visitors.length; i++) {
|
||||||
const v = visitors[i];
|
const v = visitors[i];
|
||||||
|
|
||||||
if (v.matcher(offset)) {
|
if (v.matcher(ctx.matcher, offset)) {
|
||||||
if (v.info.exit !== NOOP) {
|
if (v.info.exit !== NOOP) {
|
||||||
if (exits === null) {
|
if (exits === null) {
|
||||||
exits = [v.info.exit];
|
exits = [v.info.exit];
|
||||||
|
@ -633,7 +947,7 @@ function traverse(ctx, visitors, offset) {
|
||||||
} finally {
|
} finally {
|
||||||
if (exits !== null) {
|
if (exits !== null) {
|
||||||
for (let i = 0; i < exits.length; i++) {
|
for (let i = 0; i < exits.length; i++) {
|
||||||
const node = /** @type {*} */ (getNode(ctx, offset));
|
const node = /** @type {*} */ (getNode(ctx, originalOffset));
|
||||||
exits[i](node);
|
exits[i](node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1014
cli/js/40_lint_selector.js
Normal file
1014
cli/js/40_lint_selector.js
Normal file
File diff suppressed because it is too large
Load diff
84
cli/js/40_lint_types.d.ts
vendored
84
cli/js/40_lint_types.d.ts
vendored
|
@ -16,6 +16,7 @@ export interface AstContext {
|
||||||
strByProp: number[];
|
strByProp: number[];
|
||||||
typeByStr: Map<string, number>;
|
typeByStr: Map<string, number>;
|
||||||
propByStr: Map<string, number>;
|
propByStr: Map<string, number>;
|
||||||
|
matcher: MatchContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(@marvinhagemeister) Remove once we land "official" types
|
// TODO(@marvinhagemeister) Remove once we land "official" types
|
||||||
|
@ -43,8 +44,89 @@ export interface LintState {
|
||||||
export type VisitorFn = (node: unknown) => void;
|
export type VisitorFn = (node: unknown) => void;
|
||||||
|
|
||||||
export interface CompiledVisitor {
|
export interface CompiledVisitor {
|
||||||
matcher: (offset: number) => boolean;
|
matcher: (ctx: MatchContext, offset: number) => boolean;
|
||||||
info: { enter: VisitorFn; exit: VisitorFn };
|
info: { enter: VisitorFn; exit: VisitorFn };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AttrExists {
|
||||||
|
type: 3;
|
||||||
|
prop: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttrBin {
|
||||||
|
type: 4;
|
||||||
|
prop: number[];
|
||||||
|
op: number;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AttrSelector = AttrExists | AttrBin;
|
||||||
|
|
||||||
|
export interface ElemSelector {
|
||||||
|
type: 1;
|
||||||
|
wildcard: boolean;
|
||||||
|
elem: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PseudoNthChild {
|
||||||
|
type: 5;
|
||||||
|
op: string | null;
|
||||||
|
step: number;
|
||||||
|
stepOffset: number;
|
||||||
|
of: Selector | null;
|
||||||
|
repeat: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PseudoHas {
|
||||||
|
type: 6;
|
||||||
|
selectors: Selector[];
|
||||||
|
}
|
||||||
|
export interface PseudoNot {
|
||||||
|
type: 7;
|
||||||
|
selectors: Selector[];
|
||||||
|
}
|
||||||
|
export interface PseudoFirstChild {
|
||||||
|
type: 8;
|
||||||
|
}
|
||||||
|
export interface PseudoLastChild {
|
||||||
|
type: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Relation {
|
||||||
|
type: 2;
|
||||||
|
op: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Selector = Array<
|
||||||
|
| ElemSelector
|
||||||
|
| Relation
|
||||||
|
| AttrExists
|
||||||
|
| AttrBin
|
||||||
|
| PseudoNthChild
|
||||||
|
| PseudoNot
|
||||||
|
| PseudoHas
|
||||||
|
| PseudoFirstChild
|
||||||
|
| PseudoLastChild
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface SelectorParseCtx {
|
||||||
|
root: Selector;
|
||||||
|
current: Selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchContext {
|
||||||
|
getFirstChild(id: number): number;
|
||||||
|
getLastChild(id: number): number;
|
||||||
|
getSiblings(id: number): number[];
|
||||||
|
getParent(id: number): number;
|
||||||
|
getType(id: number): number;
|
||||||
|
hasAttrPath(id: number, propIds: number[], idx: number): boolean;
|
||||||
|
getAttrPathValue(id: number, propIds: number[], idx: number): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NextFn = (ctx: MatchContext, id: number) => boolean;
|
||||||
|
export type MatcherFn = (ctx: MatchContext, id: number) => boolean;
|
||||||
|
export type TransformFn = (value: string) => number;
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
|
@ -215,11 +215,13 @@ impl SerializeCtx {
|
||||||
let type_str = ctx.str_table.insert("type");
|
let type_str = ctx.str_table.insert("type");
|
||||||
let parent_str = ctx.str_table.insert("parent");
|
let parent_str = ctx.str_table.insert("parent");
|
||||||
let range_str = ctx.str_table.insert("range");
|
let range_str = ctx.str_table.insert("range");
|
||||||
|
let length_str = ctx.str_table.insert("length");
|
||||||
|
|
||||||
// These values are expected to be in this order on the JS side
|
// These values are expected to be in this order on the JS side
|
||||||
ctx.prop_map[0] = type_str;
|
ctx.prop_map[0] = type_str;
|
||||||
ctx.prop_map[1] = parent_str;
|
ctx.prop_map[1] = parent_str;
|
||||||
ctx.prop_map[2] = range_str;
|
ctx.prop_map[2] = range_str;
|
||||||
|
ctx.prop_map[3] = length_str;
|
||||||
|
|
||||||
ctx
|
ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,6 +205,7 @@ pub enum AstProp {
|
||||||
Type,
|
Type,
|
||||||
Parent,
|
Parent,
|
||||||
Range,
|
Range,
|
||||||
|
Length, // Not used in AST, but can be used in attr selectors
|
||||||
|
|
||||||
// Starting from here the order doesn't matter.
|
// Starting from here the order doesn't matter.
|
||||||
// Following are all possible AST node properties.
|
// Following are all possible AST node properties.
|
||||||
|
@ -320,6 +321,7 @@ impl Display for AstProp {
|
||||||
AstProp::Parent => "parent",
|
AstProp::Parent => "parent",
|
||||||
AstProp::Range => "range",
|
AstProp::Range => "range",
|
||||||
AstProp::Type => "type",
|
AstProp::Type => "type",
|
||||||
|
AstProp::Length => "length",
|
||||||
AstProp::Abstract => "abstract",
|
AstProp::Abstract => "abstract",
|
||||||
AstProp::Accessibility => "accessibility",
|
AstProp::Accessibility => "accessibility",
|
||||||
AstProp::Alternate => "alternate",
|
AstProp::Alternate => "alternate",
|
||||||
|
|
|
@ -657,6 +657,8 @@ impl CliMainWorkerFactory {
|
||||||
"40_test.js",
|
"40_test.js",
|
||||||
"40_bench.js",
|
"40_bench.js",
|
||||||
"40_jupyter.js",
|
"40_jupyter.js",
|
||||||
|
// TODO(bartlomieju): probably shouldn't include these files here?
|
||||||
|
"40_lint_selector.js",
|
||||||
"40_lint.js"
|
"40_lint.js"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ util::unit_test_factory!(
|
||||||
kv_queue_test,
|
kv_queue_test,
|
||||||
kv_queue_undelivered_test,
|
kv_queue_undelivered_test,
|
||||||
link_test,
|
link_test,
|
||||||
|
lint_selectors_test,
|
||||||
lint_plugin_test,
|
lint_plugin_test,
|
||||||
make_temp_test,
|
make_temp_test,
|
||||||
message_channel_test,
|
message_channel_test,
|
||||||
|
|
|
@ -51,22 +51,38 @@ function testPlugin(
|
||||||
return runLintPlugin(plugin, "source.tsx", source);
|
return runLintPlugin(plugin, "source.tsx", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testVisit(source: string, ...selectors: string[]): string[] {
|
interface VisitResult {
|
||||||
const log: string[] = [];
|
selector: string;
|
||||||
|
kind: "enter" | "exit";
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
node: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testVisit(
|
||||||
|
source: string,
|
||||||
|
...selectors: string[]
|
||||||
|
): VisitResult[] {
|
||||||
|
const result: VisitResult[] = [];
|
||||||
|
|
||||||
testPlugin(source, {
|
testPlugin(source, {
|
||||||
create() {
|
create() {
|
||||||
const visitor: LintVisitor = {};
|
const visitor: LintVisitor = {};
|
||||||
|
|
||||||
for (const s of selectors) {
|
for (const s of selectors) {
|
||||||
visitor[s] = () => log.push(s);
|
visitor[s] = (node) => {
|
||||||
|
result.push({
|
||||||
|
kind: s.endsWith(":exit") ? "exit" : "enter",
|
||||||
|
selector: s,
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return visitor;
|
return visitor;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return log;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLintNode(source: string, ...selectors: string[]) {
|
function testLintNode(source: string, ...selectors: string[]) {
|
||||||
|
@ -91,14 +107,188 @@ function testLintNode(source: string, ...selectors: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test("Plugin - visitor enter/exit", () => {
|
Deno.test("Plugin - visitor enter/exit", () => {
|
||||||
const enter = testVisit("foo", "Identifier");
|
const enter = testVisit(
|
||||||
assertEquals(enter, ["Identifier"]);
|
"foo",
|
||||||
|
"Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(enter[0].node.type, "Identifier");
|
||||||
|
|
||||||
const exit = testVisit("foo", "Identifier:exit");
|
const exit = testVisit(
|
||||||
assertEquals(exit, ["Identifier:exit"]);
|
"foo",
|
||||||
|
"Identifier:exit",
|
||||||
|
);
|
||||||
|
assertEquals(exit[0].node.type, "Identifier");
|
||||||
|
|
||||||
const both = testVisit("foo", "Identifier", "Identifier:exit");
|
const both = testVisit("foo", "Identifier", "Identifier:exit");
|
||||||
assertEquals(both, ["Identifier", "Identifier:exit"]);
|
assertEquals(both.map((t) => t.selector), ["Identifier", "Identifier:exit"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor descendant", () => {
|
||||||
|
let result = testVisit(
|
||||||
|
"if (false) foo; if (false) bar()",
|
||||||
|
"IfStatement CallExpression",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "CallExpression");
|
||||||
|
assertEquals(result[0].node.callee.name, "bar");
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"if (false) foo; foo()",
|
||||||
|
"IfStatement IfStatement",
|
||||||
|
);
|
||||||
|
assertEquals(result, []);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"if (false) foo; foo()",
|
||||||
|
"* CallExpression",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "CallExpression");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor child combinator", () => {
|
||||||
|
let result = testVisit(
|
||||||
|
"if (false) foo; if (false) { bar; }",
|
||||||
|
"IfStatement > ExpressionStatement > Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "foo");
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"if (false) foo; foo()",
|
||||||
|
"IfStatement IfStatement",
|
||||||
|
);
|
||||||
|
assertEquals(result, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor next sibling", () => {
|
||||||
|
const result = testVisit(
|
||||||
|
"if (false) foo; if (false) bar;",
|
||||||
|
"IfStatement + IfStatement Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor subsequent sibling", () => {
|
||||||
|
const result = testVisit(
|
||||||
|
"if (false) foo; if (false) bar; if (false) baz;",
|
||||||
|
"IfStatement ~ IfStatement Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result.map((r) => r.node.name), ["bar", "baz"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor attr", () => {
|
||||||
|
let result = testVisit(
|
||||||
|
"for (const a of b) {}",
|
||||||
|
"[await]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.await, false);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"for await (const a of b) {}",
|
||||||
|
"[await=true]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.await, true);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"for await (const a of b) {}",
|
||||||
|
"ForOfStatement[await=true]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.await, true);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"for (const a of b) {}",
|
||||||
|
"ForOfStatement[await != true]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.await, false);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"async function *foo() {}",
|
||||||
|
"FunctionDeclaration[async=true][generator=true]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.type, "FunctionDeclaration");
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"foo",
|
||||||
|
"[name='foo']",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor attr length special case", () => {
|
||||||
|
let result = testVisit(
|
||||||
|
"foo(1); foo(1, 2);",
|
||||||
|
"CallExpression[arguments.length=2]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.arguments.length, 2);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"foo(1); foo(1, 2);",
|
||||||
|
"CallExpression[arguments.length>1]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.arguments.length, 2);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"foo(1); foo(1, 2);",
|
||||||
|
"CallExpression[arguments.length<2]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.arguments.length, 1);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"foo(1); foo(1, 2);",
|
||||||
|
"CallExpression[arguments.length<=3]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.arguments.length, 1);
|
||||||
|
assertEquals(result[1].node.arguments.length, 2);
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"foo(1); foo(1, 2);",
|
||||||
|
"CallExpression[arguments.length>=1]",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.arguments.length, 1);
|
||||||
|
assertEquals(result[1].node.arguments.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor :first-child", () => {
|
||||||
|
const result = testVisit(
|
||||||
|
"{ foo; bar }",
|
||||||
|
"BlockStatement ExpressionStatement:first-child Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor :last-child", () => {
|
||||||
|
const result = testVisit(
|
||||||
|
"{ foo; bar }",
|
||||||
|
"BlockStatement ExpressionStatement:last-child Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Plugin - visitor :nth-child", () => {
|
||||||
|
let result = testVisit(
|
||||||
|
"{ foo; bar; baz; foobar; }",
|
||||||
|
"BlockStatement ExpressionStatement:nth-child(2) Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "bar");
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo; bar; baz; foobar; }",
|
||||||
|
"BlockStatement ExpressionStatement:nth-child(2n) Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "foo");
|
||||||
|
assertEquals(result[1].node.name, "baz");
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo; bar; baz; foobar; }",
|
||||||
|
"BlockStatement ExpressionStatement:nth-child(2n + 1) Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "bar");
|
||||||
|
assertEquals(result[1].node.name, "foobar");
|
||||||
|
|
||||||
|
result = testVisit(
|
||||||
|
"{ foo; bar; baz; foobar; }",
|
||||||
|
"BlockStatement *:nth-child(2n + 1 of ExpressionStatement) Identifier",
|
||||||
|
);
|
||||||
|
assertEquals(result[0].node.name, "bar");
|
||||||
|
assertEquals(result[1].node.name, "foobar");
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("Plugin - Program", () => {
|
Deno.test("Plugin - Program", () => {
|
||||||
|
|
610
tests/unit/lint_selectors_test.ts
Normal file
610
tests/unit/lint_selectors_test.ts
Normal file
|
@ -0,0 +1,610 @@
|
||||||
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
import { assertEquals } from "@std/assert/equals";
|
||||||
|
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,
|
||||||
|
splitSelectors,
|
||||||
|
Token,
|
||||||
|
} from "../../cli/js/40_lint_selector.js";
|
||||||
|
import { assertThrows } from "@std/assert";
|
||||||
|
|
||||||
|
Deno.test("splitSelectors", () => {
|
||||||
|
assertEquals(splitSelectors("foo"), ["foo"]);
|
||||||
|
assertEquals(splitSelectors("foo, bar"), ["foo", "bar"]);
|
||||||
|
assertEquals(splitSelectors("foo:f(bar, baz)"), ["foo:f(bar, baz)"]);
|
||||||
|
assertEquals(splitSelectors("foo:f(bar, baz), foobar"), [
|
||||||
|
"foo:f(bar, baz)",
|
||||||
|
"foobar",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
assertEquals(testLexer("Foo"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("foo-bar"), [
|
||||||
|
{ token: Token.Word, value: "foo-bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("foo_bar"), [
|
||||||
|
{ token: Token.Word, value: "foo_bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo Bar Baz"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Space, value: "" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
{ token: Token.Space, value: "" },
|
||||||
|
{ token: Token.Word, value: "Baz" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo Bar Baz"), [
|
||||||
|
{ 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 >", () => {
|
||||||
|
assertEquals(testLexer("Foo > Bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Op, value: ">" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo>Bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Op, value: ">" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer(">Bar"), [
|
||||||
|
{ token: Token.Op, value: ">" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Lexer - Relation +", () => {
|
||||||
|
assertEquals(testLexer("Foo + Bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Op, value: "+" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo+Bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Op, value: "+" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("+Bar"), [
|
||||||
|
{ token: Token.Op, value: "+" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Lexer - Relation ~", () => {
|
||||||
|
assertEquals(testLexer("Foo ~ Bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Op, value: "~" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo~Bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Op, value: "~" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("~Bar"), [
|
||||||
|
{ token: Token.Op, value: "~" },
|
||||||
|
{ token: Token.Word, value: "Bar" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(testLexer("Foo Bar ~ Bar"), [
|
||||||
|
{ 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", () => {
|
||||||
|
assertEquals(testLexer("[attr]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr=1]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: "=" },
|
||||||
|
{ token: Token.Word, value: "1" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr='foo']"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: "=" },
|
||||||
|
{ token: Token.String, value: "foo" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr>=2]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: ">=" },
|
||||||
|
{ token: Token.Word, value: "2" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr<=2]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: "<=" },
|
||||||
|
{ token: Token.Word, value: "2" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr>2]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: ">" },
|
||||||
|
{ token: Token.Word, value: "2" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr<2]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: "<" },
|
||||||
|
{ token: Token.Word, value: "2" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr!=2]"), [
|
||||||
|
{ token: Token.BracketOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "attr" },
|
||||||
|
{ token: Token.Op, value: "!=" },
|
||||||
|
{ token: Token.Word, value: "2" },
|
||||||
|
{ token: Token.BracketClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr.foo=1]"), [
|
||||||
|
{ 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: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("[attr] [attr]"), [
|
||||||
|
{ 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: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo[attr][attr2=1]"), [
|
||||||
|
{ 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", () => {
|
||||||
|
assertEquals(testLexer(":foo-bar"), [
|
||||||
|
{ token: Token.Colon, value: "" },
|
||||||
|
{ token: Token.Word, value: "foo-bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer("Foo:foo-bar"), [
|
||||||
|
{ token: Token.Word, value: "Foo" },
|
||||||
|
{ token: Token.Colon, value: "" },
|
||||||
|
{ token: Token.Word, value: "foo-bar" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer(":foo-bar(baz)"), [
|
||||||
|
{ token: Token.Colon, value: "" },
|
||||||
|
{ token: Token.Word, value: "foo-bar" },
|
||||||
|
{ token: Token.BraceOpen, value: "" },
|
||||||
|
{ token: Token.Word, value: "baz" },
|
||||||
|
{ token: Token.BraceClose, value: "" },
|
||||||
|
]);
|
||||||
|
assertEquals(testLexer(":foo-bar(2n + 1)"), [
|
||||||
|
{ 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", () => {
|
||||||
|
assertEquals(testParse("Foo"), [[
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 1,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Relation (descendant)", () => {
|
||||||
|
assertEquals(testParse("Foo Bar"), [[
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 1,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RELATION_NODE,
|
||||||
|
op: BinOp.Space,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 2,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Relation", () => {
|
||||||
|
assertEquals(testParse("Foo > Bar"), [[
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 1,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RELATION_NODE,
|
||||||
|
op: BinOp.Greater,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 2,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
|
||||||
|
assertEquals(testParse("Foo ~ Bar"), [[
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 1,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RELATION_NODE,
|
||||||
|
op: BinOp.Tilde,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 2,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
|
||||||
|
assertEquals(testParse("Foo + Bar"), [[
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 1,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RELATION_NODE,
|
||||||
|
op: BinOp.Plus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ELEM_NODE,
|
||||||
|
elem: 2,
|
||||||
|
wildcard: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Attr", () => {
|
||||||
|
assertEquals(testParse("[foo]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_EXISTS_NODE,
|
||||||
|
prop: [1],
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
|
||||||
|
assertEquals(testParse("[foo][bar]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_EXISTS_NODE,
|
||||||
|
prop: [1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ATTR_EXISTS_NODE,
|
||||||
|
prop: [2],
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
|
||||||
|
assertEquals(testParse("[foo=1]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse("[foo=true]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse("[foo=false]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse("[foo=null]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse("[foo='str']"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: "str",
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse('[foo="str"]'), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: "str",
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse("[foo=/str/]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: /str/,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse("[foo=/str/g]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1],
|
||||||
|
value: /str/g,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Attr nested", () => {
|
||||||
|
assertEquals(testParse("[foo.bar]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_EXISTS_NODE,
|
||||||
|
prop: [1, 2],
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
|
||||||
|
assertEquals(testParse("[foo.bar = 2]"), [[
|
||||||
|
{
|
||||||
|
type: ATTR_BIN_NODE,
|
||||||
|
op: BinOp.Equal,
|
||||||
|
prop: [1, 2],
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Pseudo no value", () => {
|
||||||
|
assertEquals(testParse(":first-child"), [[
|
||||||
|
{
|
||||||
|
type: PSEUDO_FIRST_CHILD,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":last-child"), [[
|
||||||
|
{
|
||||||
|
type: PSEUDO_LAST_CHILD,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Pseudo nth-child", () => {
|
||||||
|
assertEquals(testParse(":nth-child(2)"), [[
|
||||||
|
{
|
||||||
|
type: PSEUDO_NTH_CHILD,
|
||||||
|
of: null,
|
||||||
|
op: null,
|
||||||
|
step: 0,
|
||||||
|
stepOffset: 1,
|
||||||
|
repeat: false,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":nth-child(2n)"), [[
|
||||||
|
{
|
||||||
|
type: PSEUDO_NTH_CHILD,
|
||||||
|
of: null,
|
||||||
|
op: null,
|
||||||
|
step: 2,
|
||||||
|
stepOffset: 0,
|
||||||
|
repeat: true,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":nth-child(-2n)"), [[
|
||||||
|
{
|
||||||
|
type: PSEUDO_NTH_CHILD,
|
||||||
|
of: null,
|
||||||
|
op: null,
|
||||||
|
step: -2,
|
||||||
|
stepOffset: 0,
|
||||||
|
repeat: true,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":nth-child(2n + 1)"), [[
|
||||||
|
{
|
||||||
|
type: PSEUDO_NTH_CHILD,
|
||||||
|
of: null,
|
||||||
|
op: "+",
|
||||||
|
step: 2,
|
||||||
|
stepOffset: 1,
|
||||||
|
repeat: true,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":nth-child(2n + 1 of Foo[attr])"), [[
|
||||||
|
{
|
||||||
|
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
|
||||||
|
assertThrows(() => testParse(":nth-child(2n + 1 of Foo[attr], Bar)"));
|
||||||
|
assertThrows(() => testParse(":nth-child(2n - 1 foo)"));
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Parser - Pseudo has/is/where", () => {
|
||||||
|
assertEquals(testParse(":has(Foo:has(Foo), Bar)"), [[
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":where(Foo:where(Foo), Bar)"), [[
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
assertEquals(testParse(":is(Foo:is(Foo), Bar)"), [[
|
||||||
|
{
|
||||||
|
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", () => {
|
||||||
|
assertEquals(testParse(":not(Foo:not(Foo), Bar)"), [[
|
||||||
|
{
|
||||||
|
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", () => {
|
||||||
|
assertEquals(testParse("Foo[foo=true] Bar"), [[
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]]);
|
||||||
|
});
|
|
@ -250,6 +250,7 @@
|
||||||
"ext:deno_node/_util/std_fmt_colors.ts": "../ext/node/polyfills/_util/std_fmt_colors.ts",
|
"ext:deno_node/_util/std_fmt_colors.ts": "../ext/node/polyfills/_util/std_fmt_colors.ts",
|
||||||
"ext:deno_telemetry/telemetry.ts": "../ext/deno_telemetry/telemetry.ts",
|
"ext:deno_telemetry/telemetry.ts": "../ext/deno_telemetry/telemetry.ts",
|
||||||
"ext:deno_telemetry/util.ts": "../ext/deno_telemetry/util.ts",
|
"ext:deno_telemetry/util.ts": "../ext/deno_telemetry/util.ts",
|
||||||
|
"ext:cli/40_lint_selector.js": "../cli/js/40_lint_selector.js",
|
||||||
"@std/archive": "../tests/util/std/archive/mod.ts",
|
"@std/archive": "../tests/util/std/archive/mod.ts",
|
||||||
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
||||||
"@std/archive/untar": "../tests/util/std/archive/untar.ts",
|
"@std/archive/untar": "../tests/util/std/archive/untar.ts",
|
||||||
|
|
Loading…
Add table
Reference in a new issue