Skip to content

Commit f040d3e

Browse files
feat(jsii-reflect): add a jsii-query tool (#4952)
`jsii-query` can be used for fine-grained queries on a JSII assembly. ``` jsii-query <FILE> [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: <op><kind>[:<expression>] Where: <op> 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). <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. 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")' ``` --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0 --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 2c39079 commit f040d3e

File tree

7 files changed

+1002
-1
lines changed

7 files changed

+1002
-1
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
require('./jsii-query.js');
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import '@jsii/check-node/run';
2+
3+
import * as chalk from 'chalk';
4+
import * as yargs from 'yargs';
5+
6+
import {
7+
jsiiQuery,
8+
parseExpression,
9+
renderDocs,
10+
renderElement,
11+
} from '../lib/jsii-query';
12+
13+
async function main() {
14+
const argv = await yargs
15+
.usage(
16+
'$0 <FILE> [QUERY...]',
17+
'Queries a jsii file for its entries.',
18+
(args) =>
19+
args
20+
.positional('FILE', {
21+
type: 'string',
22+
desc: 'path to a .jsii file or directory to load',
23+
})
24+
.positional('QUERY', {
25+
type: 'string',
26+
desc: 'a query or filter expression to include or exclude items',
27+
}),
28+
)
29+
.option('types', {
30+
type: 'boolean',
31+
alias: 't',
32+
desc: 'after selecting API elements, show all selected types, as well as types containing selected members',
33+
default: false,
34+
})
35+
.option('members', {
36+
type: 'boolean',
37+
alias: 'm',
38+
desc: 'after selecting API elements, show all selected members, as well as members of selected types',
39+
default: false,
40+
})
41+
.options('docs', {
42+
type: 'boolean',
43+
alias: 'd',
44+
desc: 'show documentation for selected elements',
45+
default: false,
46+
})
47+
.option('closure', {
48+
type: 'boolean',
49+
alias: 'c',
50+
default: false,
51+
desc: 'Load dependencies of package without assuming its a JSII package itself',
52+
})
53+
.strict().epilogue(`
54+
REMARKS
55+
-------
56+
57+
There can be more than one QUERY part, which progressively filters from or adds
58+
to the list of selected elements.
59+
60+
QUERY is of the format:
61+
62+
[<op>]<kind>[:<expression>]
63+
64+
Where:
65+
66+
<op> The type of operation to apply. Absent means '.'
67+
+ Adds new API elements matching the selector to the selection.
68+
If this selects types, it also includes all type's members.
69+
- Removes API elements from the current selection that match
70+
the selector.
71+
. Removes API elements from the current selection that do NOT
72+
match the selector (i.e., retain only those that DO match
73+
the selector) (default)
74+
<kind> Type of API element to select. One of 'type' or 'member',
75+
or any of its more specific sub-types such as 'class',
76+
'interface', 'struct', 'enum', 'property', 'method', etc.
77+
Also supports aliases like 'c', 'm', 'mem', 's', 'p', etc.
78+
<expression> A JavaScript expression that will be evaluated against
79+
the member. Has access to a number of attributes like
80+
kind, ancestors, abstract, base, datatype, docs, interfaces,
81+
name, initializer, optional, overrides, protected, returns,
82+
parameters, static, variadic, type. The types are the
83+
same types as offered by the jsii-reflect class model.
84+
85+
If the first expression of the query has operator '+', then the query starts
86+
empty and the selector determines the initial set. Otherwise the query starts
87+
with all elements and the first expression is a filter on it.
88+
89+
This file evaluates the expressions as JavaScript, so this tool is not safe
90+
against untrusted input!
91+
92+
Don't forget to mind your shell escaping rules when you write query expressions.
93+
94+
Don't forget to add -- to terminate option parsing if you write negative expressions.
95+
96+
EXAMPLES
97+
-------
98+
99+
Select all enums:
100+
$ jsii-query --types node_modules/aws-cdk-lib enum
101+
102+
Select all methods with "grant" in their name:
103+
$ jsii-query --members node_modules/aws-cdk-lib 'method:name.includes("grant")'
104+
105+
Select all classes that have a grant method:
106+
$ jsii-query --types node_modules/aws-cdk-lib class 'method:name.includes("grant")'
107+
-or-
108+
$ jsii-query --types -- node_modules/aws-cdk-lib -interface 'method:name.includes("grant")'
109+
^^^^ note this
110+
111+
Select all classes that have methods that are named either 'foo' or 'bar':
112+
$ jsii-query --types node_modules/some-package '+method:name=="foo"' '+method:name=="bar"' .class
113+
114+
`).argv;
115+
116+
// Add some fields that we know are there but yargs typing doesn't know
117+
const options: typeof argv & { FILE: string; QUERY: string[] } = argv as any;
118+
119+
if (!(options.types || options.members)) {
120+
throw new Error('At least --types or --members must be specified');
121+
}
122+
123+
// Yargs is annoying; if the user uses '--' to terminate the option list,
124+
// it will not parse positionals into `QUERY` but into `_`
125+
126+
const expressions = [...options.QUERY, ...options._]
127+
.map(String)
128+
.map(parseExpression);
129+
130+
const result = await jsiiQuery({
131+
fileName: options.FILE,
132+
expressions,
133+
closure: options.closure,
134+
returnTypes: options.types,
135+
returnMembers: options.members,
136+
});
137+
138+
for (const element of result) {
139+
console.log(renderElement(element));
140+
if (options.docs) {
141+
console.log(
142+
chalk.gray(
143+
renderDocs(element)
144+
.split('\n')
145+
.map((line) => ` ${line}`)
146+
.join('\n'),
147+
),
148+
);
149+
console.log('');
150+
}
151+
}
152+
153+
process.exitCode = result.length > 0 ? 0 : 1;
154+
}
155+
156+
main().catch((e) => {
157+
console.log(e);
158+
process.exit(1);
159+
});
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
export type HierarchicalElement = string[];
2+
3+
interface TrieNode {
4+
exists: boolean;
5+
children: Trie;
6+
}
7+
type Trie = Record<string, TrieNode>;
8+
9+
export class HierarchicalSet {
10+
private root: TrieNode = {
11+
exists: false,
12+
children: {},
13+
};
14+
15+
public constructor(elements?: Iterable<HierarchicalElement>) {
16+
if (elements) {
17+
this.addAll(elements);
18+
}
19+
}
20+
21+
public addAll(elements: Iterable<HierarchicalElement>): this {
22+
for (const element of elements) {
23+
this.add(element);
24+
}
25+
return this;
26+
}
27+
28+
public add(element: HierarchicalElement): this {
29+
if (element.length === 0) {
30+
throw new Error('Elements may not be empty');
31+
}
32+
let node = this.root;
33+
for (const segment of element) {
34+
if (!(segment in node.children)) {
35+
node.children[segment] = {
36+
exists: false,
37+
children: {},
38+
};
39+
}
40+
node = node.children[segment];
41+
}
42+
node.exists = true;
43+
return this;
44+
}
45+
46+
/**
47+
* Remove every element from LHS that doesn't have a prefix in RHS
48+
*/
49+
public intersect(rhs: HierarchicalSet): this {
50+
const retainSet = new HierarchicalSet();
51+
52+
for (const el of Array.from(this)) {
53+
let found = false;
54+
for (let i = 0; i < el.length && !found; i++) {
55+
found = found || rhs.has(el.slice(0, i + 1));
56+
}
57+
if (found) {
58+
retainSet.add(el);
59+
}
60+
}
61+
62+
this.root = retainSet.root;
63+
return this;
64+
}
65+
66+
public remove(rhs: Iterable<HierarchicalElement>): this {
67+
for (const el of rhs) {
68+
const found = this.findNode(el);
69+
if (found) {
70+
const [parent, key] = found;
71+
delete parent.children[key];
72+
}
73+
}
74+
return this;
75+
}
76+
77+
public get size(): number {
78+
return Array.from(this).length;
79+
}
80+
81+
public [Symbol.iterator](): Iterator<
82+
HierarchicalElement,
83+
HierarchicalElement,
84+
any
85+
> {
86+
if (Object.keys(this.root.children).length === 0) {
87+
return {
88+
next() {
89+
return { done: true } as any;
90+
},
91+
};
92+
}
93+
94+
// A position in a trie
95+
type Cursor = { trie: Trie; keys: string[]; index: number };
96+
const stack: Cursor[] = [];
97+
function cursorFrom(node: TrieNode): Cursor {
98+
return {
99+
trie: node.children,
100+
keys: Object.keys(node.children),
101+
index: 0,
102+
};
103+
}
104+
105+
stack.push(cursorFrom(this.root));
106+
let done = false;
107+
let cur: (typeof stack)[number] = stack[stack.length - 1];
108+
109+
/**
110+
* Move 'cur' to the next node in the trie
111+
*/
112+
function advance() {
113+
// If we can descend, let's
114+
if (!isEmpty(cur.trie[cur.keys[cur.index]])) {
115+
stack.push(cursorFrom(cur.trie[cur.keys[cur.index]]));
116+
cur = stack[stack.length - 1];
117+
return;
118+
}
119+
120+
cur.index += 1;
121+
while (cur.index >= cur.keys.length) {
122+
stack.pop();
123+
if (stack.length === 0) {
124+
done = true;
125+
break;
126+
}
127+
cur = stack[stack.length - 1];
128+
// Advance the pointer after coming back.
129+
cur.index += 1;
130+
}
131+
}
132+
133+
return {
134+
next(): IteratorResult<HierarchicalElement, HierarchicalElement> {
135+
while (!done && !cur.trie[cur.keys[cur.index]].exists) {
136+
advance();
137+
}
138+
const value = !done ? stack.map((f) => f.keys[f.index]) : undefined;
139+
// Node's Array.from doesn't quite correctly implement the iterator protocol.
140+
// If we return { value: <something>, done: true } it will pretend to never
141+
// have seen <something>, so we need to split this into 2 steps.
142+
// The TypeScript typings don't agree, so 'as any' that away.
143+
const ret = (value ? { value, done } : { done }) as any;
144+
if (!done) {
145+
advance();
146+
}
147+
return ret;
148+
},
149+
};
150+
}
151+
152+
public has(el: HierarchicalElement): boolean {
153+
const found = this.findNode(el);
154+
if (!found) {
155+
return false;
156+
}
157+
const [node, last] = found;
158+
return node.children?.[last]?.exists ?? false;
159+
}
160+
161+
private findNode(el: HierarchicalElement): [TrieNode, string] | undefined {
162+
if (el.length === 0) {
163+
throw new Error('Elements may not be empty');
164+
}
165+
166+
const parts = [...el];
167+
let parent = this.root;
168+
while (parts.length > 1) {
169+
const next = parts.splice(0, 1)[0];
170+
parent = parent.children?.[next];
171+
if (!parent) {
172+
return undefined;
173+
}
174+
}
175+
176+
return [parent, parts[0]];
177+
}
178+
}
179+
180+
function isEmpty(node: TrieNode) {
181+
return Object.keys(node.children).length === 0;
182+
}

0 commit comments

Comments
 (0)