From 87a51bc7e44752bdb94cc71e88a191df9b38d25e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 16 Oct 2025 14:42:11 +0200 Subject: [PATCH 01/10] feat(jsii-reflect): add a jsii-query tool `jsii-query` can be used for fine-grained queries on a JSII assembly. ``` jsii-query [QUERY...] Queries a jsii file for its entries. Positionals: FILE path to a .jsii file or directory to load [string] QUERY a query or filter expression to include or exclude items [string] Options: --help Show help [boolean] --version Show version number [boolean] -t, --types after selecting API elements, show all selected types, as well as types containing selected members [boolean] [default: false] -m, --members after selecting API elements, show all selected members, as well as members of selected types [boolean] [default: false] -c, --closure Load dependencies of package without assuming its a JSII package itself [boolean] [default: false] REMARKS ------- There can be more than one QUERY part, which progressively filters from or adds to the list of selected elements. QUERY is of the format: [:] Where: The type of operation to apply + Adds new API elements matching the selector to the selection. If this selects types, it also includes all type's members. - Removes API elements from the current selection that match the selector. . Removes API elements from the current selection that do NOT match the selector (i.e., retain only those that DO match the selector). Type of API element to select. One of 'type' or 'member', or any of its more specific sub-types such as 'class', 'interface', 'struct', 'enum', 'property', 'method', etc. Also supports aliases like 'c', 'm', 'mem', 's', 'p', etc. A JavaScript expression that will be evaluated against the member. Has access to a number of attributes like kind, ancestors, abstract, base, datatype, docs, interfaces, name, initializer, optional, overrides, protected, returns, parameters, static, variadic, type. The types are the same types as offered by the jsii-reflect class model. This file evaluates the expressions as JavaScript, so this tool is not safe against untrusted input! EXAMPLES ------- Select all methods with "grant" in their name: $ jsii-query node_modules/aws-cdk-lib --members '.method:name.includes("grant")' ``` --- packages/jsii-reflect/bin/jsii-query | 2 + packages/jsii-reflect/bin/jsii-query.ts | 111 +++++ packages/jsii-reflect/lib/jsii-query.ts | 470 ++++++++++++++++++ packages/jsii-reflect/lib/type-system.ts | 2 +- packages/jsii-reflect/test/jsii-query.test.ts | 56 +++ 5 files changed, 640 insertions(+), 1 deletion(-) create mode 100755 packages/jsii-reflect/bin/jsii-query create mode 100644 packages/jsii-reflect/bin/jsii-query.ts create mode 100644 packages/jsii-reflect/lib/jsii-query.ts create mode 100644 packages/jsii-reflect/test/jsii-query.test.ts diff --git a/packages/jsii-reflect/bin/jsii-query b/packages/jsii-reflect/bin/jsii-query new file mode 100755 index 0000000000..c3c65eb0b9 --- /dev/null +++ b/packages/jsii-reflect/bin/jsii-query @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./jsii-query.js'); diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts new file mode 100644 index 0000000000..2c19b028ff --- /dev/null +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -0,0 +1,111 @@ +import '@jsii/check-node/run'; + +import * as chalk from 'chalk'; +import * as yargs from 'yargs'; + +import { jsiiQuery, parseExpression, renderElement } from '../lib/jsii-query'; + +async function main() { + const argv = await yargs + .usage( + '$0 [QUERY...]', + 'Queries a jsii file for its entries.', + (args) => + args + .positional('FILE', { + type: 'string', + desc: 'path to a .jsii file or directory to load', + }) + .positional('QUERY', { + type: 'string', + desc: 'a query or filter expression to include or exclude items', + }), + ) + .option('types', { + type: 'boolean', + alias: 't', + desc: 'after selecting API elements, show all selected types, as well as types containing selected members', + default: false, + }) + .option('members', { + type: 'boolean', + alias: 'm', + desc: 'after selecting API elements, show all selected members, as well as members of selected types', + default: false, + }) + .option('closure', { + type: 'boolean', + alias: 'c', + default: false, + desc: 'Load dependencies of package without assuming its a JSII package itself', + }).epilogue(` +REMARKS +------- + +There can be more than one QUERY part, which progressively filters from or adds +to the list of selected elements. + +QUERY is of the format: + + [:] + +Where: + + The type of operation to apply + + Adds new API elements matching the selector to the selection. + If this selects types, it also includes all type's members. + - Removes API elements from the current selection that match + the selector. + . Removes API elements from the current selection that do NOT + match the selector (i.e., retain only those that DO match + the selector). + Type of API element to select. One of 'type' or 'member', + or any of its more specific sub-types such as 'class', + 'interface', 'struct', 'enum', 'property', 'method', etc. + Also supports aliases like 'c', 'm', 'mem', 's', 'p', etc. + A JavaScript expression that will be evaluated against + the member. Has access to a number of attributes like + kind, ancestors, abstract, base, datatype, docs, interfaces, + name, initializer, optional, overrides, protected, returns, + parameters, static, variadic, type. + +This file evaluates the expressions as JavaScript, so this tool is not safe +against untrusted input! + +EXAMPLES +------- + +Select all methods with "grant" in their name: + +$ jsii-query node_modules/aws-cdk-lib --members '.method:name.includes("grant")' + +`).argv; + + // Add some fields that we know are there but yargs typing doesn't know + const options: typeof argv & { FILE: string; QUERY: string[] } = argv as any; + + if (!(options.types || options.members)) { + throw new Error('At least --types or --members must be specified'); + } + + const expressions = options.QUERY.map(parseExpression); + + const result = await jsiiQuery({ + fileName: options.FILE, + expressions, + closure: options.closure, + returnTypes: options.types, + returnMembers: options.members, + }); + + for (const element of result) { + console.log(renderElement(element)); + } + + process.exitCode = result.length > 0 ? 0 : 1; +} + +main().catch((e) => { + console.log(chalk.red(e)); + process.exit(1); +}); diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts new file mode 100644 index 0000000000..645551cd5a --- /dev/null +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -0,0 +1,470 @@ +/* eslint-disable @typescript-eslint/no-implied-eval */ +import '@jsii/check-node/run'; + +// Expressions: +// [:] +// +type:name == 'banana' (for 'type' also 'interface', 'class', 'enum' to imply a type check) +// -member:abstract (for 'member' also 'property', 'method', 'initializer' to imply a type check) +// .member:... +// + = select +// - = filter negative +// . = filter positive +// First query selector: start empty +// First query filter: start full + +import * as spec from '@jsii/spec'; + +import { + Callable, + ClassType, + Documentable, + EnumType, + Initializer, + InterfaceType, + Method, + Parameter, + Property, + Type, + TypeSystem, +} from '../lib'; + +const JSII_TREE_SUPPORTED_FEATURES: spec.JsiiFeature[] = ['intersection-types']; + +export interface JsiiQueryOptions { + readonly fileName: string; + readonly expressions: QExpr[]; + readonly closure?: boolean; + readonly returnTypes?: boolean; + readonly returnMembers?: boolean; +} + +export async function jsiiQuery( + options: JsiiQueryOptions, +): Promise { + const typesys = new TypeSystem(); + + if (options.closure) { + await typesys.loadNpmDependencies(options.fileName, { + validate: false, + supportedFeatures: JSII_TREE_SUPPORTED_FEATURES, + }); + } else { + await typesys.load(options.fileName, { + validate: false, + supportedFeatures: JSII_TREE_SUPPORTED_FEATURES, + }); + } + + const universe = selectAll(typesys); + + const selectedElements = selectApiElements(universe, options.expressions); + + const finalList = expandSelectToParentsAndChildren( + universe, + selectedElements, + options, + ); + + // The keys are sortable, so sort them, then get the original API elements back + // and return only those that were asked for. + + // Then retain only the kinds we asked for, and sort them + return Array.from(finalList) + .sort() + .map((key) => universe.get(key)!) + .filter( + (x) => + (isType(x) && options.returnTypes) || + (isMember(x) && options.returnMembers), + ); +} + +// - if we are asking for types, include any type that's a parent of any of the selected members +// - if we are asking for members, include all members that are a child of any of the selected types +function expandSelectToParentsAndChildren( + universe: ApiUniverse, + selected: Set, + options: JsiiQueryOptions, +): Set { + const ret = new Set(selected); + const membersForType = groupMembers(universe.keys()); + + if (options.returnTypes) { + // All type keys from either type keys or member keys + setAdd(ret, Array.from(selected).map(typeKey)); + } + if (options.returnMembers) { + // Add all member keys that are members of a selected type + for (const sel of selected) { + setAdd(ret, membersForType.get(sel) ?? []); + } + } + + return ret; +} + +function groupMembers(universeKeys: Iterable): Map { + const ret = new Map(); + for (const key of universeKeys) { + if (isTypeKey(key)) { + continue; + } + + const tk = typeKey(key); + if (!ret.has(tk)) { + ret.set(tk, []); + } + ret.get(tk)!.push(key); + } + return ret; +} + +function isType(x: ApiElement): x is Type { + return ( + x instanceof ClassType || + x instanceof InterfaceType || + x instanceof EnumType + ); +} + +function isMember(x: ApiElement): x is Callable | Property { + return x instanceof Callable || x instanceof Property; +} + +/** + * Returns a unique key per API element, used because the jsii-reflect members don't guarantee uniqueness at the object level + * + * Keys have the property that parent keys are a prefix of child keys, and that the keys are in sort order + */ +function apiElementKey(x: ApiElement): string { + if (isType(x)) { + return `${x.fqn}#`; + } + if (isMember(x)) { + const sort = + x instanceof Method && x.static + ? '000' + : x instanceof Initializer + ? '001' + : '002'; + + return `${x.parentType.fqn}#${sort}${x.name}(`; + } + throw new Error('huh'); +} + +/** + * Given a type or member key, return the type key + */ +function typeKey(x: string): string { + return `${x.split('#')[0]}#`; +} + +function isTypeKey(x: string) { + return typeKey(x) === x; +} + +function selectApiElements( + universe: ApiUniverse, + expressions: QExpr[], +): Set { + const allKeys = new Set(universe.keys()); + + let selected = + expressions.length === 0 || expressions[0].op === 'filter' + ? new Set(universe.keys()) + : new Set(); + + for (const expr of expressions) { + if (expr.op === 'filter') { + // Filter retains elements from the current set + selected = new Set(filterElements(universe, selected, expr)); + } else { + // Select adds elements (by filtering from the full set and adding to the current one) + // Selecting implicitly also adds all elements underneath + const fromUniverse = Array.from( + filterElements(universe, allKeys, { + op: 'filter', + invert: false, + kind: expr.kind, + expression: expr.expression, + }), + ); + + const newElements = Array.from(allKeys).filter((uniKey) => + fromUniverse.some((key) => uniKey.startsWith(key)), + ); + + setAdd(selected, newElements); + } + } + + return selected; +} + +function* filterElements( + universe: ApiUniverse, + elements: Set, + expr: QFilter, +): IterableIterator { + const pred = new Predicate(expr.expression, expr.invert); + + for (const key of elements) { + const el = universe.get(key); + if (!el) { + throw new Error(`Key not in universe: ${key}`); + } + if (matches(el, expr.kind, pred)) { + yield key; + } + } +} + +export class Predicate { + private readonly fn: (...args: any[]) => boolean; + + public constructor(expr?: string, invert?: boolean) { + if (!expr) { + this.fn = invert ? () => false : () => true; + } else { + const args = API_ELEMENT_ATTRIBUTES.join(','); + const neg = invert ? '!' : ''; + + const body = `return ${neg}Boolean(${expr});`; + + try { + this.fn = Function(args, body) as any; + } catch (e: any) { + throw new Error(`Syntax error in selector: ${body}: ${e}`); + } + } + } + + public apply(context: Record) { + return this.fn(...API_ELEMENT_ATTRIBUTES.map((attr) => context[attr])); + } +} + +/** + * Whether a given API element matches the filter + */ +function matches(el: ApiElement, kind: string, pred: Predicate): boolean { + const context: Record = {}; + if (el instanceof ClassType) { + if (!['type', 'class'].includes(kind)) return false; + + context.kind = 'class'; + } + if (el instanceof InterfaceType) { + const moreSpecificInterfaceType = el.datatype ? 'struct' : 'interface'; + if (!['type', moreSpecificInterfaceType].includes(kind)) return false; + + context.kind = moreSpecificInterfaceType; + } + if (el instanceof EnumType) { + if (!['type', 'enum'].includes(kind)) return false; + + context.kind = 'enum'; + } + if (el instanceof Property) { + if (!['member', 'property'].includes(kind)) return false; + + context.kind = 'property'; + } + if (el instanceof Callable) { + const moreSpecificCallable = + el instanceof Initializer ? 'initializer' : 'method'; + if (!['member', moreSpecificCallable].includes(kind)) return false; + + context.kind = moreSpecificCallable; + } + + Object.assign( + context, + Object.fromEntries( + API_ELEMENT_ATTRIBUTES.map((attr) => [attr, (el as any)[attr]]), + ), + ); + const ret = pred.apply(context); + return ret; +} + +function selectAll(typesys: TypeSystem): ApiUniverse { + return new Map( + [ + ...typesys.classes, + ...typesys.interfaces, + ...typesys.enums, + ...typesys.methods, + ...typesys.properties, + ].map((el) => [apiElementKey(el), el]), + ); +} + +type QExpr = QSelect | QFilter; + +/** + * Select adds elements + */ +interface QSelect { + readonly op: 'select'; + readonly kind: ApiKind; + readonly expression?: string; +} + +/** + * Filter retains elements + */ +interface QFilter { + readonly op: 'filter'; + readonly invert: boolean; + readonly kind: ApiKind; + readonly expression?: string; +} + +const KIND_ALIASES = { + t: 'type', + c: 'class', + i: 'interface', + s: 'struct', + e: 'enum', + p: 'property', + prop: 'property', + mem: 'member', + m: 'method', + init: 'initializer', + ctr: 'initializer', + constructor: 'initializer', +}; + +export function parseExpression(expr: string): QExpr { + if (!['-', '+', '.'].includes(expr[0])) { + throw new Error(`Invalid operator: ${expr} (must be +, - or .)`); + } + const operator = expr[0] as '-' | '+' | '.'; + const [kind_, ...expressionParts] = expr.slice(1).split(':'); + const kind = (KIND_ALIASES[kind_ as keyof typeof KIND_ALIASES] ?? + kind_) as ApiKind; + + if (!VALID_KINDS.includes(kind)) { + throw new Error( + `Invalid kind: ${kind} (must be one of ${VALID_KINDS.join(', ')})`, + ); + } + + return { + op: operator === '+' ? 'select' : 'filter', + invert: operator === '-', + kind, + expression: expressionParts?.join(':'), + }; +} + +export function renderElement(el: ApiElement) { + if (el instanceof ClassType) { + return combine(el.abstract ? 'abstract' : '', 'class', el.fqn); + } + if (el instanceof InterfaceType) { + if (el.spec.datatype) { + return combine('struct', el.fqn); + } + return combine('interface', el.fqn); + } + if (el instanceof EnumType) { + return combine('enum', el.fqn); + } + if (el instanceof Property) { + const opt = el.optional ? '?' : ''; + return combine( + el.static ? 'static' : '', + el.immutable ? 'readonly' : '', + `${el.parentType.fqn}#${el.name}${opt}: ${el.type.toString()}`, + ); + } + if (el instanceof Method) { + return combine( + el.static ? 'static' : '', + `${el.parentType.fqn}#${el.name}(${renderParams(el.parameters)}): ${el.returns.toString()}`, + ); + } + if (el instanceof Initializer) { + return `${el.parentType.fqn}(${renderParams(el.parameters)}`; + } + + return '???'; +} + +function renderParams(ps?: Parameter[]) { + return (ps ?? []) + .map((p) => { + const opt = p.optional ? '?' : ''; + const varia = p.variadic ? '...' : ''; + return `${varia}${p.name}${opt}: ${p.type.toString()}`; + }) + .join(', '); +} + +function combine(...xs: string[]) { + return xs.filter((x) => x).join(' '); +} + +function setAdd(a: Set, b: Iterable) { + for (const x of b) { + a.add(x); + } +} + +// A list of all valid API element kinds + +const VALID_KINDS = [ + // Types + 'type', + 'interface', + 'class', + 'enum', + 'struct', + // Members + 'member', + 'property', + 'method', + 'initializer', +] as const; + +type ApiKind = (typeof VALID_KINDS)[number]; + +// A list of all possible API element attributes + +type ApiElementAttribute = + | keyof ClassType + | keyof InterfaceType + | keyof EnumType + | keyof Property + | keyof Method + | keyof Initializer + | 'kind'; + +const API_ELEMENT_ATTRIBUTES: ApiElementAttribute[] = [ + 'kind', + // Types + 'ancestors', + 'abstract', + 'base', + 'datatype', + 'docs', + 'interfaces', + 'name', + // Members + 'initializer', + 'optional', + 'overrides', + 'protected', + 'returns', + 'parameters', + 'static', + 'variadic', + 'type', +]; + +type ApiElement = Documentable; + +type ApiUniverse = Map; diff --git a/packages/jsii-reflect/lib/type-system.ts b/packages/jsii-reflect/lib/type-system.ts index 6336303431..c07d38d716 100644 --- a/packages/jsii-reflect/lib/type-system.ts +++ b/packages/jsii-reflect/lib/type-system.ts @@ -65,7 +65,7 @@ export class TypeSystem { */ public async loadNpmDependencies( packageRoot: string, - options: { validate?: boolean } = {}, + options: { validate?: boolean; supportedFeatures?: JsiiFeature[] } = {}, ): Promise { const pkg = await fs.readJson(path.resolve(packageRoot, 'package.json')); diff --git a/packages/jsii-reflect/test/jsii-query.test.ts b/packages/jsii-reflect/test/jsii-query.test.ts new file mode 100644 index 0000000000..629f4c7383 --- /dev/null +++ b/packages/jsii-reflect/test/jsii-query.test.ts @@ -0,0 +1,56 @@ +import { parseExpression, Predicate } from '../lib/jsii-query'; + +describe('parseExpression', () => { + test('+', () => { + expect(parseExpression('+type')).toMatchObject({ + op: 'select', + kind: 'type', + }); + }); + test('-', () => { + expect(parseExpression('-type')).toMatchObject({ + op: 'filter', + kind: 'type', + invert: true, + }); + }); + test('.', () => { + expect(parseExpression('.type')).toMatchObject({ + op: 'filter', + kind: 'type', + invert: false, + }); + }); + + test('with expression', () => { + expect(parseExpression('.property:name.startsWith("x")')).toMatchObject({ + op: 'filter', + kind: 'property', + invert: false, + expression: 'name.startsWith("x")', + }); + }); +}); + +describe('Predicate', () => { + test('Simple', () => { + const p = new Predicate('name.startsWith("ba")'); + + expect(p.apply({ name: 'banana' })).toBeTruthy(); + expect(p.apply({ name: 'blob' })).toBeFalsy(); + }); + + test('inverted', () => { + const p = new Predicate('name.startsWith("ba")', true); + + expect(p.apply({ name: 'banana' })).toBeFalsy(); + expect(p.apply({ name: 'blob' })).toBeTruthy(); + }); + + test('empty', () => { + const p = new Predicate(); + + expect(p.apply({ name: 'banana' })).toBeTruthy(); + expect(p.apply({ name: 'blob' })).toBeTruthy(); + }); +}); From f8426b55dab7bde4f642a2863a77291a769c0e68 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 16 Oct 2025 15:44:48 +0200 Subject: [PATCH 02/10] Add a --docs feature --- packages/jsii-reflect/bin/jsii-query.ts | 27 +++++++++++++++++++++++-- packages/jsii-reflect/lib/jsii-query.ts | 4 ++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts index 2c19b028ff..372dcaa168 100644 --- a/packages/jsii-reflect/bin/jsii-query.ts +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -3,7 +3,12 @@ import '@jsii/check-node/run'; import * as chalk from 'chalk'; import * as yargs from 'yargs'; -import { jsiiQuery, parseExpression, renderElement } from '../lib/jsii-query'; +import { + jsiiQuery, + parseExpression, + renderDocs, + renderElement, +} from '../lib/jsii-query'; async function main() { const argv = await yargs @@ -33,6 +38,12 @@ async function main() { desc: 'after selecting API elements, show all selected members, as well as members of selected types', default: false, }) + .options('docs', { + type: 'boolean', + alias: 'd', + desc: 'show documentation for selected elements', + default: false, + }) .option('closure', { type: 'boolean', alias: 'c', @@ -67,7 +78,8 @@ Where: the member. Has access to a number of attributes like kind, ancestors, abstract, base, datatype, docs, interfaces, name, initializer, optional, overrides, protected, returns, - parameters, static, variadic, type. + parameters, static, variadic, type. The types are the + same types as offered by the jsii-reflect class model. This file evaluates the expressions as JavaScript, so this tool is not safe against untrusted input! @@ -100,6 +112,17 @@ $ jsii-query node_modules/aws-cdk-lib --members '.method:name.includes("grant")' for (const element of result) { console.log(renderElement(element)); + if (options.docs) { + console.log( + chalk.gray( + renderDocs(element) + .split('\n') + .map((line) => ` ${line}`) + .join('\n'), + ), + ); + console.log(''); + } } process.exitCode = result.length > 0 ? 0 : 1; diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts index 645551cd5a..b1d81505fc 100644 --- a/packages/jsii-reflect/lib/jsii-query.ts +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -394,6 +394,10 @@ export function renderElement(el: ApiElement) { return '???'; } +export function renderDocs(el: ApiElement) { + return el.docs.toString(); +} + function renderParams(ps?: Parameter[]) { return (ps ?? []) .map((p) => { From a1eedaf4712dc1e7148404df961acaab227c4ed7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 16 Oct 2025 15:50:24 +0200 Subject: [PATCH 03/10] Remind escaping --- packages/jsii-reflect/bin/jsii-query.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts index 372dcaa168..31a1dc231e 100644 --- a/packages/jsii-reflect/bin/jsii-query.ts +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -84,6 +84,8 @@ Where: This file evaluates the expressions as JavaScript, so this tool is not safe against untrusted input! +Don't forget to mind your shell escaping rules when you write query expressions. + EXAMPLES ------- From 5d038c86e15dfd10d2783724bbb20c5e428c2bb7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 16 Oct 2025 16:07:29 +0200 Subject: [PATCH 04/10] Fix filtering behavior to be more intuitive --- packages/jsii-reflect/bin/jsii-query.ts | 17 ++++++++++--- packages/jsii-reflect/lib/jsii-query.ts | 25 +++++++++++++++---- packages/jsii-reflect/test/jsii-query.test.ts | 6 +++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts index 31a1dc231e..c27478a82b 100644 --- a/packages/jsii-reflect/bin/jsii-query.ts +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -58,18 +58,18 @@ to the list of selected elements. QUERY is of the format: - [:] + [][:] Where: - The type of operation to apply + The type of operation to apply. Absent means '.' + Adds new API elements matching the selector to the selection. If this selects types, it also includes all type's members. - Removes API elements from the current selection that match the selector. . Removes API elements from the current selection that do NOT match the selector (i.e., retain only those that DO match - the selector). + the selector) (default) Type of API element to select. One of 'type' or 'member', or any of its more specific sub-types such as 'class', 'interface', 'struct', 'enum', 'property', 'method', etc. @@ -81,6 +81,10 @@ Where: parameters, static, variadic, type. The types are the same types as offered by the jsii-reflect class model. +If the first expression of the query has operator '+', then the query starts +empty and the selector determines the initial set. Otherwise the query starts +with all elements and the first expression is a filter on it. + This file evaluates the expressions as JavaScript, so this tool is not safe against untrusted input! @@ -89,9 +93,14 @@ Don't forget to mind your shell escaping rules when you write query expressions. EXAMPLES ------- +Select all enums: +$ jsii-query --types node_modules/aws-cdk-lib enum + Select all methods with "grant" in their name: +$ jsii-query --members node_modules/aws-cdk-lib 'method:name.includes("grant")' -$ jsii-query node_modules/aws-cdk-lib --members '.method:name.includes("grant")' +Select all classes that have a grant method: +$ jsii-query --types node_modules/aws-cdk-lib class 'method:name.includes("grant")' `).argv; diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts index b1d81505fc..e116b0ec99 100644 --- a/packages/jsii-reflect/lib/jsii-query.ts +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -178,7 +178,14 @@ function selectApiElements( for (const expr of expressions) { if (expr.op === 'filter') { // Filter retains elements from the current set - selected = new Set(filterElements(universe, selected, expr)); + // Filtering on types implicity filters members by that type + const retained = new Set(filterElements(universe, selected, expr)); + + selected = new Set( + Array.from(selected).filter( + (key) => retained.has(key) || retained.has(typeKey(key)), + ), + ); } else { // Select adds elements (by filtering from the full set and adding to the current one) // Selecting implicitly also adds all elements underneath @@ -338,11 +345,19 @@ const KIND_ALIASES = { }; export function parseExpression(expr: string): QExpr { - if (!['-', '+', '.'].includes(expr[0])) { - throw new Error(`Invalid operator: ${expr} (must be +, - or .)`); + let op; + if (expr[0].match(/[a-z]/i)) { + op = '.'; + } else { + op = expr[0]; + expr = expr.slice(1); + } + + if (!['-', '+', '.'].includes(op)) { + throw new Error(`Invalid operator: ${op} (must be +, - or .)`); } - const operator = expr[0] as '-' | '+' | '.'; - const [kind_, ...expressionParts] = expr.slice(1).split(':'); + const operator = op as '-' | '+' | '.'; + const [kind_, ...expressionParts] = expr.split(':'); const kind = (KIND_ALIASES[kind_ as keyof typeof KIND_ALIASES] ?? kind_) as ApiKind; diff --git a/packages/jsii-reflect/test/jsii-query.test.ts b/packages/jsii-reflect/test/jsii-query.test.ts index 629f4c7383..954833d514 100644 --- a/packages/jsii-reflect/test/jsii-query.test.ts +++ b/packages/jsii-reflect/test/jsii-query.test.ts @@ -20,6 +20,12 @@ describe('parseExpression', () => { kind: 'type', invert: false, }); + + expect(parseExpression('type')).toMatchObject({ + op: 'filter', + kind: 'type', + invert: false, + }); }); test('with expression', () => { From 872e7f42b4dfbbea0259076a7da71efc629d205d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Oct 2025 09:16:08 +0200 Subject: [PATCH 05/10] Correct handling of -- --- packages/jsii-reflect/bin/jsii-query.ts | 14 +++++- packages/jsii-reflect/lib/jsii-query.ts | 45 +++++++++++-------- packages/jsii-reflect/test/jsii-query.test.ts | 7 --- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts index c27478a82b..828028aece 100644 --- a/packages/jsii-reflect/bin/jsii-query.ts +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -49,7 +49,8 @@ async function main() { alias: 'c', default: false, desc: 'Load dependencies of package without assuming its a JSII package itself', - }).epilogue(` + }) + .strict().epilogue(` REMARKS ------- @@ -90,6 +91,8 @@ against untrusted input! Don't forget to mind your shell escaping rules when you write query expressions. +Don't forget to add -- to terminate option parsing if you write negative expressions. + EXAMPLES ------- @@ -101,6 +104,8 @@ $ jsii-query --members node_modules/aws-cdk-lib 'method:name.includes("grant")' Select all classes that have a grant method: $ jsii-query --types node_modules/aws-cdk-lib class 'method:name.includes("grant")' +OR: +$ jsii-query --types -- node_modules/aws-cdk-lib -interface 'method:name.includes("grant")' `).argv; @@ -111,7 +116,12 @@ $ jsii-query --types node_modules/aws-cdk-lib class 'method:name.includes("grant throw new Error('At least --types or --members must be specified'); } - const expressions = options.QUERY.map(parseExpression); + // Yargs is annoying; if the user uses '--' to terminate the option list, + // it will not parse positionals into `QUERY` but into `_` + + const expressions = [...options.QUERY, ...options._] + .map(String) + .map(parseExpression); const result = await jsiiQuery({ fileName: options.FILE, diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts index e116b0ec99..0441e65132 100644 --- a/packages/jsii-reflect/lib/jsii-query.ts +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -179,23 +179,26 @@ function selectApiElements( if (expr.op === 'filter') { // Filter retains elements from the current set // Filtering on types implicity filters members by that type - const retained = new Set(filterElements(universe, selected, expr)); + const filteredFqns = new Set( + filterElements(universe, selected, expr.kind, expr.expression), + ); - selected = new Set( + const filtered = new Set( Array.from(selected).filter( - (key) => retained.has(key) || retained.has(typeKey(key)), + (key) => filteredFqns.has(key) || filteredFqns.has(typeKey(key)), ), ); + + if (expr.remove) { + setRemove(selected, filtered); + } else { + selected = filtered; + } } else { // Select adds elements (by filtering from the full set and adding to the current one) // Selecting implicitly also adds all elements underneath const fromUniverse = Array.from( - filterElements(universe, allKeys, { - op: 'filter', - invert: false, - kind: expr.kind, - expression: expr.expression, - }), + filterElements(universe, allKeys, expr.kind, expr.expression), ); const newElements = Array.from(allKeys).filter((uniKey) => @@ -212,16 +215,17 @@ function selectApiElements( function* filterElements( universe: ApiUniverse, elements: Set, - expr: QFilter, + kind: ApiKind, + expression?: string, ): IterableIterator { - const pred = new Predicate(expr.expression, expr.invert); + const pred = new Predicate(expression); for (const key of elements) { const el = universe.get(key); if (!el) { throw new Error(`Key not in universe: ${key}`); } - if (matches(el, expr.kind, pred)) { + if (matches(el, kind, pred)) { yield key; } } @@ -230,14 +234,13 @@ function* filterElements( export class Predicate { private readonly fn: (...args: any[]) => boolean; - public constructor(expr?: string, invert?: boolean) { + public constructor(expr?: string) { if (!expr) { - this.fn = invert ? () => false : () => true; + this.fn = () => true; } else { const args = API_ELEMENT_ATTRIBUTES.join(','); - const neg = invert ? '!' : ''; - const body = `return ${neg}Boolean(${expr});`; + const body = `return Boolean(${expr});`; try { this.fn = Function(args, body) as any; @@ -324,7 +327,7 @@ interface QSelect { */ interface QFilter { readonly op: 'filter'; - readonly invert: boolean; + readonly remove: boolean; readonly kind: ApiKind; readonly expression?: string; } @@ -369,7 +372,7 @@ export function parseExpression(expr: string): QExpr { return { op: operator === '+' ? 'select' : 'filter', - invert: operator === '-', + remove: operator === '-', kind, expression: expressionParts?.join(':'), }; @@ -433,6 +436,12 @@ function setAdd(a: Set, b: Iterable) { } } +function setRemove(a: Set, b: Iterable) { + for (const x of b) { + a.delete(x); + } +} + // A list of all valid API element kinds const VALID_KINDS = [ diff --git a/packages/jsii-reflect/test/jsii-query.test.ts b/packages/jsii-reflect/test/jsii-query.test.ts index 954833d514..75365fc9c8 100644 --- a/packages/jsii-reflect/test/jsii-query.test.ts +++ b/packages/jsii-reflect/test/jsii-query.test.ts @@ -46,13 +46,6 @@ describe('Predicate', () => { expect(p.apply({ name: 'blob' })).toBeFalsy(); }); - test('inverted', () => { - const p = new Predicate('name.startsWith("ba")', true); - - expect(p.apply({ name: 'banana' })).toBeFalsy(); - expect(p.apply({ name: 'blob' })).toBeTruthy(); - }); - test('empty', () => { const p = new Predicate(); From 8f4425c6724389cd0143e32b9042efb8f14d66c9 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 17 Oct 2025 14:55:16 +0200 Subject: [PATCH 06/10] MOre natural set semantics --- packages/jsii-reflect/bin/jsii-query.ts | 6 +- packages/jsii-reflect/lib/hierarchical-set.ts | 159 ++++++++++++++++++ packages/jsii-reflect/lib/jsii-query.ts | 146 ++++++---------- .../test/hierarchical-set.test.ts | 62 +++++++ 4 files changed, 277 insertions(+), 96 deletions(-) create mode 100644 packages/jsii-reflect/lib/hierarchical-set.ts create mode 100644 packages/jsii-reflect/test/hierarchical-set.test.ts diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts index 828028aece..c2c49488de 100644 --- a/packages/jsii-reflect/bin/jsii-query.ts +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -104,8 +104,12 @@ $ jsii-query --members node_modules/aws-cdk-lib 'method:name.includes("grant")' Select all classes that have a grant method: $ jsii-query --types node_modules/aws-cdk-lib class 'method:name.includes("grant")' -OR: + -or- $ jsii-query --types -- node_modules/aws-cdk-lib -interface 'method:name.includes("grant")' + ^^^^ note this + +Select all classes that have methods that are named either 'foo' or 'bar': +$ jsii-query --types node_modules/some-package '+method:name=="foo"' '+method:name=="bar"' .class `).argv; diff --git a/packages/jsii-reflect/lib/hierarchical-set.ts b/packages/jsii-reflect/lib/hierarchical-set.ts new file mode 100644 index 0000000000..2276c08c17 --- /dev/null +++ b/packages/jsii-reflect/lib/hierarchical-set.ts @@ -0,0 +1,159 @@ +export type HierarchicalElement = string[]; + +interface TrieNode { + exists: boolean; + children: Trie; +} +type Trie = Record; + +export class HierarchicalSet { + private readonly root: TrieNode = { + exists: false, + children: {}, + }; + + public constructor(elements?: Iterable) { + if (elements) { + this.add(elements); + } + } + + public add(elements: Iterable): this { + for (const element of elements) { + if (element.length === 0) { + throw new Error('Elements may not be empty'); + } + let node = this.root; + for (const segment of element) { + if (!(segment in node.children)) { + node.children[segment] = { + exists: false, + children: {}, + }; + } + node = node.children[segment]; + } + node.exists = true; + } + return this; + } + + /** + * Remove every element from LHS that doesn't have a prefix in RHS + */ + public intersect(rhs: HierarchicalSet): this { + for (const el of Array.from(this)) { + let found = false; + for (let i = 0; i < el.length && !found; i++) { + found = found || rhs.has(el.slice(0, i + 1)); + } + if (!found) { + this.remove([el]); + } + } + return this; + } + + public remove(rhs: Iterable): this { + for (const el of rhs) { + const found = this.findNode(el); + if (found) { + const [parent, key] = found; + delete parent.children[key]; + } + } + return this; + } + + public get size(): number { + return Array.from(this).length; + } + + public [Symbol.iterator](): Iterator< + HierarchicalElement, + HierarchicalElement, + any + > { + const stack: Array<{ trie: Trie; keys: string[]; index: number }> = []; + stack.push({ + trie: this.root.children, + keys: Object.keys(this.root.children), + index: 0, + }); + let done = false; + let cur: (typeof stack)[number] = stack[stack.length - 1]; + + /** + * Move 'cur' to the next node in the trie + */ + function advance() { + // If we can descend, let's + if (Object.keys(cur.trie[cur.keys[cur.index]].children).length > 0) { + stack.push({ + trie: cur.trie[cur.keys[cur.index]].children, + index: 0, + keys: Object.keys(cur.trie[cur.keys[cur.index]].children), + }); + cur = stack[stack.length - 1]; + return; + } + + cur.index += 1; + while (cur.index >= cur.keys.length) { + stack.pop(); + if (stack.length === 0) { + done = true; + break; + } + cur = stack[stack.length - 1]; + // Advance the pointer after coming back. + cur.index += 1; + } + } + + return { + next(): IteratorResult { + while (!done && !cur.trie[cur.keys[cur.index]].exists) { + advance(); + } + const value = !done ? stack.map((f) => f.keys[f.index]) : undefined; + // Node's Array.from doesn't quite correctly implement the iterator protocol. + // If we return { value: , done: true } it will pretend to never + // have seen , so we need to split this into 2 steps. + // The TypeScript typings don't agree, so 'as any' that away. + const ret = (value ? { value, done } : { done }) as any; + if (!done) { + advance(); + } + return ret; + }, + }; + } + + public has(el: HierarchicalElement): boolean { + const found = this.findNode(el); + if (!found) { + return false; + } + const [node, last] = found; + return node.children?.[last]?.exists ?? false; + } + + private findNode(el: HierarchicalElement): [TrieNode, string] | undefined { + if (el.length === 0) { + throw new Error('Elements may not be empty'); + } + + const parts = [...el]; + let parent = this.root; + while (parts.length > 1) { + const next = parts.splice(0, 1)[0]; + parent = parent.children?.[next]; + if (!parent) { + return undefined; + } + } + + return [parent, parts[0]]; + } +} diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts index 0441e65132..e91eb7ba26 100644 --- a/packages/jsii-reflect/lib/jsii-query.ts +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -27,6 +27,7 @@ import { Type, TypeSystem, } from '../lib'; +import { HierarchicalElement, HierarchicalSet } from './hierarchical-set'; const JSII_TREE_SUPPORTED_FEATURES: spec.JsiiFeature[] = ['intersection-types']; @@ -59,19 +60,15 @@ export async function jsiiQuery( const selectedElements = selectApiElements(universe, options.expressions); - const finalList = expandSelectToParentsAndChildren( - universe, - selectedElements, - options, - ); + expandSelectToParentsAndChildren(universe, selectedElements, options); // The keys are sortable, so sort them, then get the original API elements back // and return only those that were asked for. // Then retain only the kinds we asked for, and sort them - return Array.from(finalList) + return Array.from(selectedElements) .sort() - .map((key) => universe.get(key)!) + .map((key) => universe.get(stringFromKey(key))!) .filter( (x) => (isType(x) && options.returnTypes) || @@ -83,40 +80,22 @@ export async function jsiiQuery( // - if we are asking for members, include all members that are a child of any of the selected types function expandSelectToParentsAndChildren( universe: ApiUniverse, - selected: Set, + selected: HierarchicalSet, options: JsiiQueryOptions, -): Set { - const ret = new Set(selected); - const membersForType = groupMembers(universe.keys()); - +) { if (options.returnTypes) { // All type keys from either type keys or member keys - setAdd(ret, Array.from(selected).map(typeKey)); + selected.add(Array.from(selected).map(typeKey)); } - if (options.returnMembers) { - // Add all member keys that are members of a selected type - for (const sel of selected) { - setAdd(ret, membersForType.get(sel) ?? []); - } - } - - return ret; -} -function groupMembers(universeKeys: Iterable): Map { - const ret = new Map(); - for (const key of universeKeys) { - if (isTypeKey(key)) { - continue; - } + if (options.returnMembers) { + const allElements = new HierarchicalSet( + Array.from(universe.keys()).map(keyFromString), + ); - const tk = typeKey(key); - if (!ret.has(tk)) { - ret.set(tk, []); - } - ret.get(tk)!.push(key); + // Add all member keys that are members of a selected type + selected.add(new HierarchicalSet(allElements).intersect(selected)); } - return ret; } function isType(x: ApiElement): x is Type { @@ -136,9 +115,9 @@ function isMember(x: ApiElement): x is Callable | Property { * * Keys have the property that parent keys are a prefix of child keys, and that the keys are in sort order */ -function apiElementKey(x: ApiElement): string { +function apiElementKey(x: ApiElement): HierarchicalElement { if (isType(x)) { - return `${x.fqn}#`; + return [`${x.fqn}`]; } if (isMember(x)) { const sort = @@ -148,82 +127,71 @@ function apiElementKey(x: ApiElement): string { ? '001' : '002'; - return `${x.parentType.fqn}#${sort}${x.name}(`; + return [`${x.parentType.fqn}`, `${sort}${x.name}`]; } throw new Error('huh'); } +function stringFromKey(x: HierarchicalElement) { + return x.map((s) => `${s}#`).join(''); +} + +function keyFromString(x: string): HierarchicalElement { + return x.split('#').slice(0, -1); +} + /** * Given a type or member key, return the type key */ -function typeKey(x: string): string { - return `${x.split('#')[0]}#`; -} - -function isTypeKey(x: string) { - return typeKey(x) === x; +function typeKey(x: HierarchicalElement): string[] { + return [x[0]]; } function selectApiElements( universe: ApiUniverse, expressions: QExpr[], -): Set { - const allKeys = new Set(universe.keys()); +): HierarchicalSet { + const allKeys = new HierarchicalSet( + Array.from(universe.keys()).map(keyFromString), + ); - let selected = + const currentSelection = expressions.length === 0 || expressions[0].op === 'filter' - ? new Set(universe.keys()) - : new Set(); + ? new HierarchicalSet(allKeys) + : new HierarchicalSet(); for (const expr of expressions) { - if (expr.op === 'filter') { - // Filter retains elements from the current set - // Filtering on types implicity filters members by that type - const filteredFqns = new Set( - filterElements(universe, selected, expr.kind, expr.expression), - ); - - const filtered = new Set( - Array.from(selected).filter( - (key) => filteredFqns.has(key) || filteredFqns.has(typeKey(key)), - ), - ); - - if (expr.remove) { - setRemove(selected, filtered); - } else { - selected = filtered; - } - } else { - // Select adds elements (by filtering from the full set and adding to the current one) - // Selecting implicitly also adds all elements underneath - const fromUniverse = Array.from( - filterElements(universe, allKeys, expr.kind, expr.expression), - ); - - const newElements = Array.from(allKeys).filter((uniKey) => - fromUniverse.some((key) => uniKey.startsWith(key)), - ); + const thisQuery = filterElements( + universe, + allKeys, + expr.kind, + expr.expression, + ); - setAdd(selected, newElements); + if (expr.op === 'filter' && expr.remove) { + currentSelection.remove(thisQuery); + } else if (expr.op === 'filter') { + currentSelection.intersect(new HierarchicalSet(thisQuery)); + } else { + currentSelection.add(thisQuery); } } - return selected; + return currentSelection; } function* filterElements( universe: ApiUniverse, - elements: Set, + elements: HierarchicalSet, kind: ApiKind, expression?: string, -): IterableIterator { +): Iterable { const pred = new Predicate(expression); for (const key of elements) { - const el = universe.get(key); + const el = universe.get(stringFromKey(key)); if (!el) { - throw new Error(`Key not in universe: ${key}`); + throw new Error(`Key not in universe: ${stringFromKey(key)}`); } if (matches(el, kind, pred)) { yield key; @@ -307,7 +275,7 @@ function selectAll(typesys: TypeSystem): ApiUniverse { ...typesys.enums, ...typesys.methods, ...typesys.properties, - ].map((el) => [apiElementKey(el), el]), + ].map((el) => [stringFromKey(apiElementKey(el)), el]), ); } @@ -430,18 +398,6 @@ function combine(...xs: string[]) { return xs.filter((x) => x).join(' '); } -function setAdd(a: Set, b: Iterable) { - for (const x of b) { - a.add(x); - } -} - -function setRemove(a: Set, b: Iterable) { - for (const x of b) { - a.delete(x); - } -} - // A list of all valid API element kinds const VALID_KINDS = [ diff --git a/packages/jsii-reflect/test/hierarchical-set.test.ts b/packages/jsii-reflect/test/hierarchical-set.test.ts new file mode 100644 index 0000000000..5b0a2ceeb0 --- /dev/null +++ b/packages/jsii-reflect/test/hierarchical-set.test.ts @@ -0,0 +1,62 @@ +import { HierarchicalSet } from '../lib/hierarchical-set'; + +test('set iteration', () => { + const hs = new HierarchicalSet([ + ['a'], + ['a', 'b', 'c'], + ['a', 'b', 'd'], + ['a', 'e'], + ['f'], + ]); + + expect(Array.from(hs)).toEqual([ + ['a'], + ['a', 'b', 'c'], + ['a', 'b', 'd'], + ['a', 'e'], + ['f'], + ]); +}); + +test('add prefix after child', () => { + const hs = new HierarchicalSet(); + hs.add([['a', 'b']]); + hs.add([['a']]); + + expect(Array.from(hs)).toEqual([['a'], ['a', 'b']]); +}); + +describe('remove', () => { + test('remove literals', () => { + const x = new HierarchicalSet([['a', 'b'], ['c'], ['d']]); + + x.remove([['a', 'b'], ['c']]); + + expect(Array.from(x)).toEqual([['d']]); + }); + + test('remove parents', () => { + const x = new HierarchicalSet([['a', 'b'], ['c'], ['d']]); + + x.remove([['a']]); + + expect(Array.from(x)).toEqual([['c'], ['d']]); + }); +}); + +describe('intersect', () => { + test('retains literal elements', () => { + const x = new HierarchicalSet([['a', 'b'], ['c'], ['d']]); + x.intersect(new HierarchicalSet([['a', 'b'], ['c']])); + + expect(Array.from(x)).toEqual([['a', 'b'], ['c']]); + }); + + test('retains children of parents', () => { + const x = new HierarchicalSet([['a', 'b'], ['c'], ['d']]); + + x.intersect(new HierarchicalSet([['a']])); + + expect(Array.from(x)).toEqual([['a', 'b']]); + }); +}); From 7e402501b7b79d1922702f8a6ee6914690e3692a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 Oct 2025 10:59:07 +0200 Subject: [PATCH 07/10] Fix --- packages/jsii-reflect/bin/jsii-query.ts | 2 +- packages/jsii-reflect/lib/hierarchical-set.ts | 83 +++++++++++------- packages/jsii-reflect/lib/jsii-query.ts | 16 ++-- .../test/hierarchical-set.test.ts | 14 ++- packages/jsii-reflect/test/jsii-query.test.ts | 86 +++++++++++++++++-- 5 files changed, 154 insertions(+), 47 deletions(-) diff --git a/packages/jsii-reflect/bin/jsii-query.ts b/packages/jsii-reflect/bin/jsii-query.ts index c2c49488de..50382a4551 100644 --- a/packages/jsii-reflect/bin/jsii-query.ts +++ b/packages/jsii-reflect/bin/jsii-query.ts @@ -154,6 +154,6 @@ $ jsii-query --types node_modules/some-package '+method:name=="foo"' '+method:na } main().catch((e) => { - console.log(chalk.red(e)); + console.log(e); process.exit(1); }); diff --git a/packages/jsii-reflect/lib/hierarchical-set.ts b/packages/jsii-reflect/lib/hierarchical-set.ts index 2276c08c17..98b3560b79 100644 --- a/packages/jsii-reflect/lib/hierarchical-set.ts +++ b/packages/jsii-reflect/lib/hierarchical-set.ts @@ -7,34 +7,39 @@ interface TrieNode { type Trie = Record; export class HierarchicalSet { - private readonly root: TrieNode = { + private root: TrieNode = { exists: false, children: {}, }; public constructor(elements?: Iterable) { if (elements) { - this.add(elements); + this.addAll(elements); } } - public add(elements: Iterable): this { + public addAll(elements: Iterable): this { for (const element of elements) { - if (element.length === 0) { - throw new Error('Elements may not be empty'); - } - let node = this.root; - for (const segment of element) { - if (!(segment in node.children)) { - node.children[segment] = { - exists: false, - children: {}, - }; - } - node = node.children[segment]; + this.add(element); + } + return this; + } + + public add(element: HierarchicalElement): this { + if (element.length === 0) { + throw new Error('Elements may not be empty'); + } + let node = this.root; + for (const segment of element) { + if (!(segment in node.children)) { + node.children[segment] = { + exists: false, + children: {}, + }; } - node.exists = true; + node = node.children[segment]; } + node.exists = true; return this; } @@ -42,15 +47,19 @@ export class HierarchicalSet { * Remove every element from LHS that doesn't have a prefix in RHS */ public intersect(rhs: HierarchicalSet): this { + const retainSet = new HierarchicalSet(); + for (const el of Array.from(this)) { let found = false; for (let i = 0; i < el.length && !found; i++) { found = found || rhs.has(el.slice(0, i + 1)); } - if (!found) { - this.remove([el]); + if (found) { + retainSet.add(el); } } + + this.root = retainSet.root; return this; } @@ -74,12 +83,26 @@ export class HierarchicalSet { HierarchicalElement, any > { - const stack: Array<{ trie: Trie; keys: string[]; index: number }> = []; - stack.push({ - trie: this.root.children, - keys: Object.keys(this.root.children), - index: 0, - }); + if (Object.keys(this.root.children).length === 0) { + return { + next() { + return { done: true } as any; + }, + }; + } + + // A position in a trie + type Cursor = { trie: Trie; keys: string[]; index: number }; + const stack: Cursor[] = []; + function cursorFrom(node: TrieNode): Cursor { + return { + trie: node.children, + keys: Object.keys(node.children), + index: 0, + }; + } + + stack.push(cursorFrom(this.root)); let done = false; let cur: (typeof stack)[number] = stack[stack.length - 1]; @@ -88,12 +111,8 @@ export class HierarchicalSet { */ function advance() { // If we can descend, let's - if (Object.keys(cur.trie[cur.keys[cur.index]].children).length > 0) { - stack.push({ - trie: cur.trie[cur.keys[cur.index]].children, - index: 0, - keys: Object.keys(cur.trie[cur.keys[cur.index]].children), - }); + if (!isEmpty(cur.trie[cur.keys[cur.index]])) { + stack.push(cursorFrom(cur.trie[cur.keys[cur.index]])); cur = stack[stack.length - 1]; return; } @@ -157,3 +176,7 @@ export class HierarchicalSet { return [parent, parts[0]]; } } + +function isEmpty(node: TrieNode) { + return Object.keys(node.children).length === 0; +} diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts index e91eb7ba26..888aa53fd6 100644 --- a/packages/jsii-reflect/lib/jsii-query.ts +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -85,7 +85,7 @@ function expandSelectToParentsAndChildren( ) { if (options.returnTypes) { // All type keys from either type keys or member keys - selected.add(Array.from(selected).map(typeKey)); + selected.addAll(Array.from(selected).map(typeKey)); } if (options.returnMembers) { @@ -94,7 +94,7 @@ function expandSelectToParentsAndChildren( ); // Add all member keys that are members of a selected type - selected.add(new HierarchicalSet(allElements).intersect(selected)); + selected.addAll(new HierarchicalSet(allElements).intersect(selected)); } } @@ -161,11 +161,8 @@ function selectApiElements( : new HierarchicalSet(); for (const expr of expressions) { - const thisQuery = filterElements( - universe, - allKeys, - expr.kind, - expr.expression, + const thisQuery = Array.from( + filterElements(universe, allKeys, expr.kind, expr.expression), ); if (expr.op === 'filter' && expr.remove) { @@ -173,7 +170,7 @@ function selectApiElements( } else if (expr.op === 'filter') { currentSelection.intersect(new HierarchicalSet(thisQuery)); } else { - currentSelection.add(thisQuery); + currentSelection.addAll(thisQuery); } } @@ -389,7 +386,8 @@ function renderParams(ps?: Parameter[]) { .map((p) => { const opt = p.optional ? '?' : ''; const varia = p.variadic ? '...' : ''; - return `${varia}${p.name}${opt}: ${p.type.toString()}`; + const arr = p.variadic ? '[]' : ''; + return `${varia}${p.name}${opt}: ${p.type.toString()}${arr}`; }) .join(', '); } diff --git a/packages/jsii-reflect/test/hierarchical-set.test.ts b/packages/jsii-reflect/test/hierarchical-set.test.ts index 5b0a2ceeb0..088d14ef21 100644 --- a/packages/jsii-reflect/test/hierarchical-set.test.ts +++ b/packages/jsii-reflect/test/hierarchical-set.test.ts @@ -1,5 +1,9 @@ import { HierarchicalSet } from '../lib/hierarchical-set'; +test('empty set', () => { + expect(Array.from(new HierarchicalSet())).toEqual([]); +}); + test('set iteration', () => { const hs = new HierarchicalSet([ ['a'], @@ -20,8 +24,8 @@ test('set iteration', () => { test('add prefix after child', () => { const hs = new HierarchicalSet(); - hs.add([['a', 'b']]); - hs.add([['a']]); + hs.addAll([['a', 'b']]); + hs.addAll([['a']]); expect(Array.from(hs)).toEqual([['a'], ['a', 'b']]); }); @@ -59,4 +63,10 @@ describe('intersect', () => { expect(Array.from(x)).toEqual([['a', 'b']]); }); + + test('parent and child only retains child', () => { + const x = new HierarchicalSet([['a'], ['a', 'b']]); + x.intersect(new HierarchicalSet([['a', 'b'], ['c']])); + expect(Array.from(x)).toEqual([['a', 'b']]); + }); }); diff --git a/packages/jsii-reflect/test/jsii-query.test.ts b/packages/jsii-reflect/test/jsii-query.test.ts index 75365fc9c8..9e72d68fbf 100644 --- a/packages/jsii-reflect/test/jsii-query.test.ts +++ b/packages/jsii-reflect/test/jsii-query.test.ts @@ -1,4 +1,12 @@ -import { parseExpression, Predicate } from '../lib/jsii-query'; +import * as path from 'path'; + +import { + jsiiQuery, + JsiiQueryOptions, + parseExpression, + Predicate, + renderElement, +} from '../lib/jsii-query'; describe('parseExpression', () => { test('+', () => { @@ -11,20 +19,20 @@ describe('parseExpression', () => { expect(parseExpression('-type')).toMatchObject({ op: 'filter', kind: 'type', - invert: true, + remove: true, }); }); test('.', () => { expect(parseExpression('.type')).toMatchObject({ op: 'filter', kind: 'type', - invert: false, + remove: false, }); expect(parseExpression('type')).toMatchObject({ op: 'filter', kind: 'type', - invert: false, + remove: false, }); }); @@ -32,7 +40,7 @@ describe('parseExpression', () => { expect(parseExpression('.property:name.startsWith("x")')).toMatchObject({ op: 'filter', kind: 'property', - invert: false, + remove: false, expression: 'name.startsWith("x")', }); }); @@ -53,3 +61,71 @@ describe('Predicate', () => { expect(p.apply({ name: 'blob' })).toBeTruthy(); }); }); + +describe('filtering', () => { + test('empty filter returns everything', async () => { + const result = await query([], 'members'); + expect(result.length).toBeGreaterThan(700); + expect(result).toContainEqual( + 'readonly jsii-calc.ExportedBaseClass#success: boolean', + ); + }); + + test('filter on method name', async () => { + const result = await query( + [parseExpression('method:name.includes("con")')], + 'members', + ); + expect(result).toContainEqual( + 'static @scope/jsii-calc-base-of-base.StaticConsumer#consume(..._args: any[]): void', + ); + }); + + test('filter on methods but expect types', async () => { + const result = await query( + [parseExpression('method:name.includes("con")')], + 'types', + ); + expect(result).toContainEqual( + 'class @scope/jsii-calc-base-of-base.StaticConsumer', + ); + }); + + test('filter on type but expect members', async () => { + const result = await query( + [parseExpression('class:name == "StaticConsumer"')], + 'members', + ); + expect(result).toContainEqual( + 'static @scope/jsii-calc-base-of-base.StaticConsumer#consume(..._args: any[]): void', + ); + }); + + test('filter on classes with a separate expression', async () => { + const result = await query( + [ + parseExpression('method:name.includes("con")'), + parseExpression('class'), + ], + 'members', + ); + expect(result).toContainEqual( + 'static @scope/jsii-calc-base-of-base.StaticConsumer#consume(..._args: any[]): void', + ); + }); +}); + +async function query( + exp: JsiiQueryOptions['expressions'], + what: 'members' | 'types' | 'all' = 'all', +): Promise { + const jsiiCalcDir = path.dirname(require.resolve('jsii-calc/package.json')); + + const result = await jsiiQuery({ + fileName: jsiiCalcDir, + expressions: exp, + returnMembers: what !== 'types', + returnTypes: what !== 'members', + }); + return result.map(renderElement); +} From 6fd81cd0e5d88b754c7f5bd4ca0e002131162d56 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 Oct 2025 11:54:03 +0200 Subject: [PATCH 08/10] Adopt featuire --- packages/jsii-reflect/test/features.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/jsii-reflect/test/features.ts b/packages/jsii-reflect/test/features.ts index c4487f8f5c..d6e9ab9501 100644 --- a/packages/jsii-reflect/test/features.ts +++ b/packages/jsii-reflect/test/features.ts @@ -1,3 +1,6 @@ import { JsiiFeature } from '@jsii/spec'; -export const TEST_FEATURES: JsiiFeature[] = ['intersection-types']; +export const TEST_FEATURES: JsiiFeature[] = [ + 'intersection-types', + 'class-covariant-overrides', +]; From 077c531b63908155edbc5664a8db6b9e6e869de6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 Oct 2025 12:48:50 +0200 Subject: [PATCH 09/10] Snaps --- .../test/__snapshots__/jsii-tree.test.js.snap | 120 ------------------ .../test/__snapshots__/tree.test.js.snap | 76 ----------- .../__snapshots__/type-system.test.js.snap | 6 - 3 files changed, 202 deletions(-) diff --git a/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap b/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap index ed39730fbf..749cd04ce1 100644 --- a/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap @@ -174,58 +174,6 @@ exports[`jsii-tree --all 1`] = ` │ │ │ └─┬ enum CompositionStringStyle (stable) │ │ │ ├── NORMAL (stable) │ │ │ └── DECORATED (stable) - │ │ ├─┬ covariantOverrides - │ │ │ ├─┬ submodules - │ │ │ │ └─┬ classOverrides - │ │ │ │ └─┬ types - │ │ │ │ ├─┬ class Base (stable) - │ │ │ │ │ ├── interfaces: IBase - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer (stable) - │ │ │ │ │ ├─┬ createSomething() method (stable) - │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.Superclass - │ │ │ │ │ ├─┬ list property (stable) - │ │ │ │ │ │ ├── immutable - │ │ │ │ │ │ └── type: Array - │ │ │ │ │ └─┬ something property (stable) - │ │ │ │ │ ├── immutable - │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass - │ │ │ │ ├─┬ class Derived (stable) - │ │ │ │ │ ├── base: Middle - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer (stable) - │ │ │ │ │ ├─┬ createSomething() method (stable) - │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.SubSubclass - │ │ │ │ │ ├─┬ list property (stable) - │ │ │ │ │ │ ├── immutable - │ │ │ │ │ │ └── type: Array - │ │ │ │ │ └─┬ something property (stable) - │ │ │ │ │ ├── immutable - │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.SubSubclass - │ │ │ │ ├─┬ class Middle (stable) - │ │ │ │ │ ├── base: Base - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer (stable) - │ │ │ │ │ └─┬ addUnrelatedMember property (stable) - │ │ │ │ │ └── type: number - │ │ │ │ ├─┬ class SubSubclass (stable) - │ │ │ │ │ ├── base: Subclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer (stable) - │ │ │ │ ├─┬ class Subclass (stable) - │ │ │ │ │ ├── base: Superclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer (stable) - │ │ │ │ ├─┬ class Superclass (stable) - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer (stable) - │ │ │ │ └─┬ interface IBase (stable) - │ │ │ │ └─┬ members - │ │ │ │ └─┬ something property (stable) - │ │ │ │ ├── abstract - │ │ │ │ ├── immutable - │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass - │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -3866,23 +3814,6 @@ exports[`jsii-tree --inheritance 1`] = ` │ │ │ ├─┬ class CompositeOperation │ │ │ │ └── base: Operation │ │ │ └── enum CompositionStringStyle - │ │ ├─┬ covariantOverrides - │ │ │ ├─┬ submodules - │ │ │ │ └─┬ classOverrides - │ │ │ │ └─┬ types - │ │ │ │ ├─┬ class Base - │ │ │ │ │ └── interfaces: IBase - │ │ │ │ ├─┬ class Derived - │ │ │ │ │ └── base: Middle - │ │ │ │ ├─┬ class Middle - │ │ │ │ │ └── base: Base - │ │ │ │ ├─┬ class SubSubclass - │ │ │ │ │ └── base: Subclass - │ │ │ │ ├─┬ class Subclass - │ │ │ │ │ └── base: Superclass - │ │ │ │ ├── class Superclass - │ │ │ │ └── interface IBase - │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -4488,39 +4419,6 @@ exports[`jsii-tree --members 1`] = ` │ │ │ └─┬ enum CompositionStringStyle │ │ │ ├── NORMAL │ │ │ └── DECORATED - │ │ ├─┬ covariantOverrides - │ │ │ ├─┬ submodules - │ │ │ │ └─┬ classOverrides - │ │ │ │ └─┬ types - │ │ │ │ ├─┬ class Base - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer - │ │ │ │ │ ├── createSomething() method - │ │ │ │ │ ├── list property - │ │ │ │ │ └── something property - │ │ │ │ ├─┬ class Derived - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer - │ │ │ │ │ ├── createSomething() method - │ │ │ │ │ ├── list property - │ │ │ │ │ └── something property - │ │ │ │ ├─┬ class Middle - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer - │ │ │ │ │ └── addUnrelatedMember property - │ │ │ │ ├─┬ class SubSubclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer - │ │ │ │ ├─┬ class Subclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer - │ │ │ │ ├─┬ class Superclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer - │ │ │ │ └─┬ interface IBase - │ │ │ │ └─┬ members - │ │ │ │ └── something property - │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -6162,9 +6060,6 @@ exports[`jsii-tree --signatures 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition - │ ├─┬ covariantOverrides - │ │ └─┬ submodules - │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -6254,18 +6149,6 @@ exports[`jsii-tree --types 1`] = ` │ │ │ └─┬ types │ │ │ ├── class CompositeOperation │ │ │ └── enum CompositionStringStyle - │ │ ├─┬ covariantOverrides - │ │ │ ├─┬ submodules - │ │ │ │ └─┬ classOverrides - │ │ │ │ └─┬ types - │ │ │ │ ├── class Base - │ │ │ │ ├── class Derived - │ │ │ │ ├── class Middle - │ │ │ │ ├── class SubSubclass - │ │ │ │ ├── class Subclass - │ │ │ │ ├── class Superclass - │ │ │ │ └── interface IBase - │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -6688,9 +6571,6 @@ exports[`jsii-tree 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition - │ ├─┬ covariantOverrides - │ │ └─┬ submodules - │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar diff --git a/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap b/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap index dc7de3e9f3..d030fca510 100644 --- a/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap @@ -14,9 +14,6 @@ exports[`defaults 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition - │ ├─┬ covariantOverrides - │ │ └─┬ submodules - │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -79,9 +76,6 @@ exports[`inheritance 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition - │ ├─┬ covariantOverrides - │ │ └─┬ submodules - │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -144,9 +138,6 @@ exports[`members 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition - │ ├─┬ covariantOverrides - │ │ └─┬ submodules - │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -369,58 +360,6 @@ exports[`showAll 1`] = ` │ │ │ └─┬ enum CompositionStringStyle │ │ │ ├── NORMAL │ │ │ └── DECORATED - │ │ ├─┬ covariantOverrides - │ │ │ ├─┬ submodules - │ │ │ │ └─┬ classOverrides - │ │ │ │ └─┬ types - │ │ │ │ ├─┬ class Base - │ │ │ │ │ ├── interfaces: IBase - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer - │ │ │ │ │ ├─┬ createSomething() method - │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.Superclass - │ │ │ │ │ ├─┬ list property - │ │ │ │ │ │ ├── immutable - │ │ │ │ │ │ └── type: Array - │ │ │ │ │ └─┬ something property - │ │ │ │ │ ├── immutable - │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass - │ │ │ │ ├─┬ class Derived - │ │ │ │ │ ├── base: Middle - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer - │ │ │ │ │ ├─┬ createSomething() method - │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.SubSubclass - │ │ │ │ │ ├─┬ list property - │ │ │ │ │ │ ├── immutable - │ │ │ │ │ │ └── type: Array - │ │ │ │ │ └─┬ something property - │ │ │ │ │ ├── immutable - │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.SubSubclass - │ │ │ │ ├─┬ class Middle - │ │ │ │ │ ├── base: Base - │ │ │ │ │ └─┬ members - │ │ │ │ │ ├── () initializer - │ │ │ │ │ └─┬ addUnrelatedMember property - │ │ │ │ │ └── type: number - │ │ │ │ ├─┬ class SubSubclass - │ │ │ │ │ ├── base: Subclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer - │ │ │ │ ├─┬ class Subclass - │ │ │ │ │ ├── base: Superclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer - │ │ │ │ ├─┬ class Superclass - │ │ │ │ │ └─┬ members - │ │ │ │ │ └── () initializer - │ │ │ │ └─┬ interface IBase - │ │ │ │ └─┬ members - │ │ │ │ └─┬ something property - │ │ │ │ ├── abstract - │ │ │ │ ├── immutable - │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass - │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -4031,9 +3970,6 @@ exports[`signatures 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition - │ ├─┬ covariantOverrides - │ │ └─┬ submodules - │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -4123,18 +4059,6 @@ exports[`types 1`] = ` │ │ │ └─┬ types │ │ │ ├── class CompositeOperation │ │ │ └── enum CompositionStringStyle - │ │ ├─┬ covariantOverrides - │ │ │ ├─┬ submodules - │ │ │ │ └─┬ classOverrides - │ │ │ │ └─┬ types - │ │ │ │ ├── class Base - │ │ │ │ ├── class Derived - │ │ │ │ ├── class Middle - │ │ │ │ ├── class SubSubclass - │ │ │ │ ├── class Subclass - │ │ │ │ ├── class Superclass - │ │ │ │ └── interface IBase - │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar diff --git a/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap b/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap index ad333348dc..648fe2aff2 100644 --- a/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap @@ -174,12 +174,6 @@ exports[`TypeSystem.classes lists all the classes in the typesystem 1`] = ` "jsii-calc.cdk16625.donotimport.UnimportedSubmoduleType", "jsii-calc.cdk22369.AcceptsPath", "jsii-calc.composition.CompositeOperation", - "jsii-calc.covariantOverrides.classOverrides.Base", - "jsii-calc.covariantOverrides.classOverrides.Derived", - "jsii-calc.covariantOverrides.classOverrides.Middle", - "jsii-calc.covariantOverrides.classOverrides.SubSubclass", - "jsii-calc.covariantOverrides.classOverrides.Subclass", - "jsii-calc.covariantOverrides.classOverrides.Superclass", "jsii-calc.homonymousForwardReferences.bar.Consumer", "jsii-calc.homonymousForwardReferences.foo.Consumer", "jsii-calc.intersection.ConsumesIntersection", From e9b4fba041f86788f844f71d0569f3658c4ec52a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 23 Oct 2025 14:02:54 +0200 Subject: [PATCH 10/10] Snaps and feature --- packages/jsii-reflect/lib/jsii-query.ts | 5 +- .../test/__snapshots__/jsii-tree.test.js.snap | 120 ++++++++++++++++++ .../test/__snapshots__/tree.test.js.snap | 76 +++++++++++ .../__snapshots__/type-system.test.js.snap | 6 + 4 files changed, 206 insertions(+), 1 deletion(-) diff --git a/packages/jsii-reflect/lib/jsii-query.ts b/packages/jsii-reflect/lib/jsii-query.ts index 888aa53fd6..cb01c04485 100644 --- a/packages/jsii-reflect/lib/jsii-query.ts +++ b/packages/jsii-reflect/lib/jsii-query.ts @@ -29,7 +29,10 @@ import { } from '../lib'; import { HierarchicalElement, HierarchicalSet } from './hierarchical-set'; -const JSII_TREE_SUPPORTED_FEATURES: spec.JsiiFeature[] = ['intersection-types']; +const JSII_TREE_SUPPORTED_FEATURES: spec.JsiiFeature[] = [ + 'intersection-types', + 'class-covariant-overrides', +]; export interface JsiiQueryOptions { readonly fileName: string; diff --git a/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap b/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap index 749cd04ce1..ed39730fbf 100644 --- a/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap @@ -174,6 +174,58 @@ exports[`jsii-tree --all 1`] = ` │ │ │ └─┬ enum CompositionStringStyle (stable) │ │ │ ├── NORMAL (stable) │ │ │ └── DECORATED (stable) + │ │ ├─┬ covariantOverrides + │ │ │ ├─┬ submodules + │ │ │ │ └─┬ classOverrides + │ │ │ │ └─┬ types + │ │ │ │ ├─┬ class Base (stable) + │ │ │ │ │ ├── interfaces: IBase + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer (stable) + │ │ │ │ │ ├─┬ createSomething() method (stable) + │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.Superclass + │ │ │ │ │ ├─┬ list property (stable) + │ │ │ │ │ │ ├── immutable + │ │ │ │ │ │ └── type: Array + │ │ │ │ │ └─┬ something property (stable) + │ │ │ │ │ ├── immutable + │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass + │ │ │ │ ├─┬ class Derived (stable) + │ │ │ │ │ ├── base: Middle + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer (stable) + │ │ │ │ │ ├─┬ createSomething() method (stable) + │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.SubSubclass + │ │ │ │ │ ├─┬ list property (stable) + │ │ │ │ │ │ ├── immutable + │ │ │ │ │ │ └── type: Array + │ │ │ │ │ └─┬ something property (stable) + │ │ │ │ │ ├── immutable + │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.SubSubclass + │ │ │ │ ├─┬ class Middle (stable) + │ │ │ │ │ ├── base: Base + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer (stable) + │ │ │ │ │ └─┬ addUnrelatedMember property (stable) + │ │ │ │ │ └── type: number + │ │ │ │ ├─┬ class SubSubclass (stable) + │ │ │ │ │ ├── base: Subclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer (stable) + │ │ │ │ ├─┬ class Subclass (stable) + │ │ │ │ │ ├── base: Superclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer (stable) + │ │ │ │ ├─┬ class Superclass (stable) + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer (stable) + │ │ │ │ └─┬ interface IBase (stable) + │ │ │ │ └─┬ members + │ │ │ │ └─┬ something property (stable) + │ │ │ │ ├── abstract + │ │ │ │ ├── immutable + │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass + │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -3814,6 +3866,23 @@ exports[`jsii-tree --inheritance 1`] = ` │ │ │ ├─┬ class CompositeOperation │ │ │ │ └── base: Operation │ │ │ └── enum CompositionStringStyle + │ │ ├─┬ covariantOverrides + │ │ │ ├─┬ submodules + │ │ │ │ └─┬ classOverrides + │ │ │ │ └─┬ types + │ │ │ │ ├─┬ class Base + │ │ │ │ │ └── interfaces: IBase + │ │ │ │ ├─┬ class Derived + │ │ │ │ │ └── base: Middle + │ │ │ │ ├─┬ class Middle + │ │ │ │ │ └── base: Base + │ │ │ │ ├─┬ class SubSubclass + │ │ │ │ │ └── base: Subclass + │ │ │ │ ├─┬ class Subclass + │ │ │ │ │ └── base: Superclass + │ │ │ │ ├── class Superclass + │ │ │ │ └── interface IBase + │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -4419,6 +4488,39 @@ exports[`jsii-tree --members 1`] = ` │ │ │ └─┬ enum CompositionStringStyle │ │ │ ├── NORMAL │ │ │ └── DECORATED + │ │ ├─┬ covariantOverrides + │ │ │ ├─┬ submodules + │ │ │ │ └─┬ classOverrides + │ │ │ │ └─┬ types + │ │ │ │ ├─┬ class Base + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer + │ │ │ │ │ ├── createSomething() method + │ │ │ │ │ ├── list property + │ │ │ │ │ └── something property + │ │ │ │ ├─┬ class Derived + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer + │ │ │ │ │ ├── createSomething() method + │ │ │ │ │ ├── list property + │ │ │ │ │ └── something property + │ │ │ │ ├─┬ class Middle + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer + │ │ │ │ │ └── addUnrelatedMember property + │ │ │ │ ├─┬ class SubSubclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer + │ │ │ │ ├─┬ class Subclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer + │ │ │ │ ├─┬ class Superclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer + │ │ │ │ └─┬ interface IBase + │ │ │ │ └─┬ members + │ │ │ │ └── something property + │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -6060,6 +6162,9 @@ exports[`jsii-tree --signatures 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition + │ ├─┬ covariantOverrides + │ │ └─┬ submodules + │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -6149,6 +6254,18 @@ exports[`jsii-tree --types 1`] = ` │ │ │ └─┬ types │ │ │ ├── class CompositeOperation │ │ │ └── enum CompositionStringStyle + │ │ ├─┬ covariantOverrides + │ │ │ ├─┬ submodules + │ │ │ │ └─┬ classOverrides + │ │ │ │ └─┬ types + │ │ │ │ ├── class Base + │ │ │ │ ├── class Derived + │ │ │ │ ├── class Middle + │ │ │ │ ├── class SubSubclass + │ │ │ │ ├── class Subclass + │ │ │ │ ├── class Superclass + │ │ │ │ └── interface IBase + │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -6571,6 +6688,9 @@ exports[`jsii-tree 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition + │ ├─┬ covariantOverrides + │ │ └─┬ submodules + │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar diff --git a/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap b/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap index d030fca510..dc7de3e9f3 100644 --- a/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap @@ -14,6 +14,9 @@ exports[`defaults 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition + │ ├─┬ covariantOverrides + │ │ └─┬ submodules + │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -76,6 +79,9 @@ exports[`inheritance 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition + │ ├─┬ covariantOverrides + │ │ └─┬ submodules + │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -138,6 +144,9 @@ exports[`members 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition + │ ├─┬ covariantOverrides + │ │ └─┬ submodules + │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -360,6 +369,58 @@ exports[`showAll 1`] = ` │ │ │ └─┬ enum CompositionStringStyle │ │ │ ├── NORMAL │ │ │ └── DECORATED + │ │ ├─┬ covariantOverrides + │ │ │ ├─┬ submodules + │ │ │ │ └─┬ classOverrides + │ │ │ │ └─┬ types + │ │ │ │ ├─┬ class Base + │ │ │ │ │ ├── interfaces: IBase + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer + │ │ │ │ │ ├─┬ createSomething() method + │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.Superclass + │ │ │ │ │ ├─┬ list property + │ │ │ │ │ │ ├── immutable + │ │ │ │ │ │ └── type: Array + │ │ │ │ │ └─┬ something property + │ │ │ │ │ ├── immutable + │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass + │ │ │ │ ├─┬ class Derived + │ │ │ │ │ ├── base: Middle + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer + │ │ │ │ │ ├─┬ createSomething() method + │ │ │ │ │ │ └── returns: jsii-calc.covariantOverrides.classOverrides.SubSubclass + │ │ │ │ │ ├─┬ list property + │ │ │ │ │ │ ├── immutable + │ │ │ │ │ │ └── type: Array + │ │ │ │ │ └─┬ something property + │ │ │ │ │ ├── immutable + │ │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.SubSubclass + │ │ │ │ ├─┬ class Middle + │ │ │ │ │ ├── base: Base + │ │ │ │ │ └─┬ members + │ │ │ │ │ ├── () initializer + │ │ │ │ │ └─┬ addUnrelatedMember property + │ │ │ │ │ └── type: number + │ │ │ │ ├─┬ class SubSubclass + │ │ │ │ │ ├── base: Subclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer + │ │ │ │ ├─┬ class Subclass + │ │ │ │ │ ├── base: Superclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer + │ │ │ │ ├─┬ class Superclass + │ │ │ │ │ └─┬ members + │ │ │ │ │ └── () initializer + │ │ │ │ └─┬ interface IBase + │ │ │ │ └─┬ members + │ │ │ │ └─┬ something property + │ │ │ │ ├── abstract + │ │ │ │ ├── immutable + │ │ │ │ └── type: jsii-calc.covariantOverrides.classOverrides.Superclass + │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar @@ -3970,6 +4031,9 @@ exports[`signatures 1`] = ` │ │ └── donotimport │ ├── cdk22369 │ ├── composition + │ ├─┬ covariantOverrides + │ │ └─┬ submodules + │ │ └── classOverrides │ ├─┬ homonymousForwardReferences │ │ └─┬ submodules │ │ ├── bar @@ -4059,6 +4123,18 @@ exports[`types 1`] = ` │ │ │ └─┬ types │ │ │ ├── class CompositeOperation │ │ │ └── enum CompositionStringStyle + │ │ ├─┬ covariantOverrides + │ │ │ ├─┬ submodules + │ │ │ │ └─┬ classOverrides + │ │ │ │ └─┬ types + │ │ │ │ ├── class Base + │ │ │ │ ├── class Derived + │ │ │ │ ├── class Middle + │ │ │ │ ├── class SubSubclass + │ │ │ │ ├── class Subclass + │ │ │ │ ├── class Superclass + │ │ │ │ └── interface IBase + │ │ │ └── types │ │ ├─┬ homonymousForwardReferences │ │ │ ├─┬ submodules │ │ │ │ ├─┬ bar diff --git a/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap b/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap index 648fe2aff2..ad333348dc 100644 --- a/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap @@ -174,6 +174,12 @@ exports[`TypeSystem.classes lists all the classes in the typesystem 1`] = ` "jsii-calc.cdk16625.donotimport.UnimportedSubmoduleType", "jsii-calc.cdk22369.AcceptsPath", "jsii-calc.composition.CompositeOperation", + "jsii-calc.covariantOverrides.classOverrides.Base", + "jsii-calc.covariantOverrides.classOverrides.Derived", + "jsii-calc.covariantOverrides.classOverrides.Middle", + "jsii-calc.covariantOverrides.classOverrides.SubSubclass", + "jsii-calc.covariantOverrides.classOverrides.Subclass", + "jsii-calc.covariantOverrides.classOverrides.Superclass", "jsii-calc.homonymousForwardReferences.bar.Consumer", "jsii-calc.homonymousForwardReferences.foo.Consumer", "jsii-calc.intersection.ConsumesIntersection",