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
|
||||
|
||||
import {
|
||||
compileSelector,
|
||||
parseSelector,
|
||||
splitSelectors,
|
||||
} from "ext:cli/40_lint_selector.js";
|
||||
import { core, internals } from "ext:core/mod.js";
|
||||
const {
|
||||
op_lint_create_serialized_ast,
|
||||
|
@ -13,6 +18,7 @@ const {
|
|||
const AST_PROP_TYPE = 0;
|
||||
const AST_PROP_PARENT = 1;
|
||||
const AST_PROP_RANGE = 2;
|
||||
const AST_PROP_LENGTH = 3;
|
||||
|
||||
// Keep in sync with Rust
|
||||
// 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").NodeFacade} NodeFacade */
|
||||
/** @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").TestReportData} TestReportData */
|
||||
/** @typedef {import("./40_lint_types.d.ts").TransformFn} TransformFn */
|
||||
/** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchContext */
|
||||
|
||||
/** @type {LintState} */
|
||||
const state = {
|
||||
|
@ -99,7 +105,6 @@ export function installPlugin(plugin) {
|
|||
*/
|
||||
function getNode(ctx, offset) {
|
||||
if (offset === 0) return null;
|
||||
|
||||
const cached = ctx.nodes.get(offset);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
|
@ -297,9 +302,10 @@ function readValue(ctx, offset, search) {
|
|||
if (offset === -1) return undefined;
|
||||
|
||||
const kind = buf[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (kind === PropFlags.Ref) {
|
||||
const value = readU32(buf, offset + 2);
|
||||
const value = readU32(buf, offset);
|
||||
return getNode(ctx, value);
|
||||
} else if (kind === PropFlags.RefArr) {
|
||||
const len = readU32(buf, offset);
|
||||
|
@ -353,6 +359,303 @@ function getString(strTable, id) {
|
|||
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 {AstContext} buf
|
||||
|
@ -433,6 +736,7 @@ function createAstContext(buf) {
|
|||
strByType,
|
||||
typeByStr,
|
||||
propByStr,
|
||||
matcher: new MatchCtx(buf, strTable),
|
||||
};
|
||||
|
||||
setNodeGetters(ctx);
|
||||
|
@ -456,7 +760,7 @@ const NOOP = (_node) => {};
|
|||
export function runPluginsForFile(fileName, serializedAst) {
|
||||
const ctx = createAstContext(serializedAst);
|
||||
|
||||
/** @type {Map<string, { enter: VisitorFn, exit: VisitorFn}>} */
|
||||
/** @type {Map<string, CompiledVisitor["info"]>}>} */
|
||||
const bySelector = new Map();
|
||||
|
||||
const destroyFns = [];
|
||||
|
@ -486,6 +790,11 @@ export function runPluginsForFile(fileName, serializedAst) {
|
|||
key = key.slice(0, -":exit".length);
|
||||
}
|
||||
|
||||
const selectors = splitSelectors(key);
|
||||
|
||||
for (let j = 0; j < selectors.length; j++) {
|
||||
const key = selectors[j];
|
||||
|
||||
let info = bySelector.get(key);
|
||||
if (info === undefined) {
|
||||
info = { enter: NOOP, exit: NOOP };
|
||||
|
@ -514,6 +823,7 @@ export function runPluginsForFile(fileName, serializedAst) {
|
|||
info.enter = wrapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof rule.destroy === "function") {
|
||||
const destroyFn = rule.destroy.bind(rule);
|
||||
|
@ -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[]} */
|
||||
const visitors = [];
|
||||
for (const [sel, info] of bySelector.entries()) {
|
||||
// This will make more sense once selectors land as it's faster
|
||||
// to precompile them once upfront.
|
||||
// Selectors are already split here.
|
||||
// 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
|
||||
// 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;
|
||||
},
|
||||
});
|
||||
visitors.push({ info, matcher });
|
||||
}
|
||||
|
||||
// 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
|
||||
if (offset === 0) return;
|
||||
|
||||
const originalOffset = offset;
|
||||
|
||||
const { buf } = ctx;
|
||||
|
||||
/** @type {VisitorFn[] | null} */
|
||||
|
@ -580,7 +894,7 @@ function traverse(ctx, visitors, offset) {
|
|||
for (let i = 0; i < visitors.length; i++) {
|
||||
const v = visitors[i];
|
||||
|
||||
if (v.matcher(offset)) {
|
||||
if (v.matcher(ctx.matcher, offset)) {
|
||||
if (v.info.exit !== NOOP) {
|
||||
if (exits === null) {
|
||||
exits = [v.info.exit];
|
||||
|
@ -633,7 +947,7 @@ function traverse(ctx, visitors, offset) {
|
|||
} finally {
|
||||
if (exits !== null) {
|
||||
for (let i = 0; i < exits.length; i++) {
|
||||
const node = /** @type {*} */ (getNode(ctx, offset));
|
||||
const node = /** @type {*} */ (getNode(ctx, originalOffset));
|
||||
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[];
|
||||
typeByStr: Map<string, number>;
|
||||
propByStr: Map<string, number>;
|
||||
matcher: MatchContext;
|
||||
}
|
||||
|
||||
// TODO(@marvinhagemeister) Remove once we land "official" types
|
||||
|
@ -43,8 +44,89 @@ export interface LintState {
|
|||
export type VisitorFn = (node: unknown) => void;
|
||||
|
||||
export interface CompiledVisitor {
|
||||
matcher: (offset: number) => boolean;
|
||||
matcher: (ctx: MatchContext, offset: number) => boolean;
|
||||
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 {};
|
||||
|
|
|
@ -215,11 +215,13 @@ impl SerializeCtx {
|
|||
let type_str = ctx.str_table.insert("type");
|
||||
let parent_str = ctx.str_table.insert("parent");
|
||||
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
|
||||
ctx.prop_map[0] = type_str;
|
||||
ctx.prop_map[1] = parent_str;
|
||||
ctx.prop_map[2] = range_str;
|
||||
ctx.prop_map[3] = length_str;
|
||||
|
||||
ctx
|
||||
}
|
||||
|
|
|
@ -205,6 +205,7 @@ pub enum AstProp {
|
|||
Type,
|
||||
Parent,
|
||||
Range,
|
||||
Length, // Not used in AST, but can be used in attr selectors
|
||||
|
||||
// Starting from here the order doesn't matter.
|
||||
// Following are all possible AST node properties.
|
||||
|
@ -320,6 +321,7 @@ impl Display for AstProp {
|
|||
AstProp::Parent => "parent",
|
||||
AstProp::Range => "range",
|
||||
AstProp::Type => "type",
|
||||
AstProp::Length => "length",
|
||||
AstProp::Abstract => "abstract",
|
||||
AstProp::Accessibility => "accessibility",
|
||||
AstProp::Alternate => "alternate",
|
||||
|
|
|
@ -657,6 +657,8 @@ impl CliMainWorkerFactory {
|
|||
"40_test.js",
|
||||
"40_bench.js",
|
||||
"40_jupyter.js",
|
||||
// TODO(bartlomieju): probably shouldn't include these files here?
|
||||
"40_lint_selector.js",
|
||||
"40_lint.js"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ util::unit_test_factory!(
|
|||
kv_queue_test,
|
||||
kv_queue_undelivered_test,
|
||||
link_test,
|
||||
lint_selectors_test,
|
||||
lint_plugin_test,
|
||||
make_temp_test,
|
||||
message_channel_test,
|
||||
|
|
|
@ -51,22 +51,38 @@ function testPlugin(
|
|||
return runLintPlugin(plugin, "source.tsx", source);
|
||||
}
|
||||
|
||||
function testVisit(source: string, ...selectors: string[]): string[] {
|
||||
const log: string[] = [];
|
||||
interface VisitResult {
|
||||
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, {
|
||||
create() {
|
||||
const visitor: LintVisitor = {};
|
||||
|
||||
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 log;
|
||||
return result;
|
||||
}
|
||||
|
||||
function testLintNode(source: string, ...selectors: string[]) {
|
||||
|
@ -91,14 +107,188 @@ function testLintNode(source: string, ...selectors: string[]) {
|
|||
}
|
||||
|
||||
Deno.test("Plugin - visitor enter/exit", () => {
|
||||
const enter = testVisit("foo", "Identifier");
|
||||
assertEquals(enter, ["Identifier"]);
|
||||
const enter = testVisit(
|
||||
"foo",
|
||||
"Identifier",
|
||||
);
|
||||
assertEquals(enter[0].node.type, "Identifier");
|
||||
|
||||
const exit = testVisit("foo", "Identifier:exit");
|
||||
assertEquals(exit, ["Identifier:exit"]);
|
||||
const exit = testVisit(
|
||||
"foo",
|
||||
"Identifier:exit",
|
||||
);
|
||||
assertEquals(exit[0].node.type, "Identifier");
|
||||
|
||||
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", () => {
|
||||
|
|
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_telemetry/telemetry.ts": "../ext/deno_telemetry/telemetry.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/tar": "../tests/util/std/archive/tar.ts",
|
||||
"@std/archive/untar": "../tests/util/std/archive/untar.ts",
|
||||
|
|
Loading…
Add table
Reference in a new issue