Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/jsii-reflect/bin/jsii-query
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('./jsii-query.js');
159 changes: 159 additions & 0 deletions packages/jsii-reflect/bin/jsii-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import '@jsii/check-node/run';

import * as chalk from 'chalk';
import * as yargs from 'yargs';

import {
jsiiQuery,
parseExpression,
renderDocs,
renderElement,
} from '../lib/jsii-query';

async function main() {
const argv = await yargs
.usage(
'$0 <FILE> [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,
})
.options('docs', {
type: 'boolean',
alias: 'd',
desc: 'show documentation for selected elements',
default: false,
})
.option('closure', {
type: 'boolean',
alias: 'c',
default: false,
desc: 'Load dependencies of package without assuming its a JSII package itself',
})
.strict().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:

[<op>]<kind>[:<expression>]

Where:

<op> 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) (default)
<kind> 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.
<expression> 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.

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!

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
-------

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")'

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")'
^^^^ 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;

// 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');
}

// 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,
expressions,
closure: options.closure,
returnTypes: options.types,
returnMembers: options.members,
});

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;
}

main().catch((e) => {
console.log(e);
process.exit(1);
});
182 changes: 182 additions & 0 deletions packages/jsii-reflect/lib/hierarchical-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
export type HierarchicalElement = string[];

interface TrieNode {
exists: boolean;
children: Trie;
}
type Trie = Record<string, TrieNode>;

export class HierarchicalSet {
private root: TrieNode = {
exists: false,
children: {},
};

public constructor(elements?: Iterable<HierarchicalElement>) {
if (elements) {
this.addAll(elements);
}
}

public addAll(elements: Iterable<HierarchicalElement>): this {
for (const element of elements) {
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 = 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 {
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) {
retainSet.add(el);
}
}

this.root = retainSet.root;
return this;
}

public remove(rhs: Iterable<HierarchicalElement>): 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
> {
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];

/**
* Move 'cur' to the next node in the trie
*/
function advance() {
// If we can descend, let's
if (!isEmpty(cur.trie[cur.keys[cur.index]])) {
stack.push(cursorFrom(cur.trie[cur.keys[cur.index]]));
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<HierarchicalElement, HierarchicalElement> {
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: <something>, done: true } it will pretend to never
// have seen <something>, 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]];
}
}

function isEmpty(node: TrieNode) {
return Object.keys(node.children).length === 0;
}
Loading
Loading