@vltpkg/query 1.0.0-rc.32 → 1.0.0-rc.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +10 -6
- package/dist/parser.js +2 -1
- package/dist/pseudo/hostname.js +3 -1
- package/dist/pseudo/malware.d.ts +3 -16
- package/dist/pseudo/malware.js +11 -171
- package/dist/pseudo/scripts.js +1 -1
- package/dist/pseudo/vuln.d.ts +22 -0
- package/dist/pseudo/vuln.js +220 -0
- package/dist/pseudo.js +3 -3
- package/dist/types.d.ts +3 -2
- package/package.json +19 -10
- package/skills/dss-query/REFERENCE.md +123 -0
- package/skills/dss-query/SKILL.md +190 -0
- package/skills/dss-query/evals/README.md +79 -0
- package/skills/dss-query/evals/evals.json +89 -0
- package/skills/dss-query/evals/grade.mjs +480 -0
- package/src/attribute.ts +186 -0
- package/src/combinator.ts +132 -0
- package/src/id.ts +46 -0
- package/src/index.ts +620 -0
- package/src/parser.ts +122 -0
- package/src/pseudo/abandoned.ts +9 -0
- package/src/pseudo/attr.ts +92 -0
- package/src/pseudo/built.ts +19 -0
- package/src/pseudo/confused.ts +29 -0
- package/src/pseudo/cve.ts +93 -0
- package/src/pseudo/cwe.ts +97 -0
- package/src/pseudo/debug.ts +9 -0
- package/src/pseudo/deprecated.ts +9 -0
- package/src/pseudo/dev.ts +18 -0
- package/src/pseudo/diff.ts +90 -0
- package/src/pseudo/dist.ts +144 -0
- package/src/pseudo/dynamic.ts +9 -0
- package/src/pseudo/empty.ts +15 -0
- package/src/pseudo/entropic.ts +9 -0
- package/src/pseudo/env.ts +6 -0
- package/src/pseudo/eval.ts +9 -0
- package/src/pseudo/fs.ts +9 -0
- package/src/pseudo/helpers.ts +106 -0
- package/src/pseudo/host.ts +111 -0
- package/src/pseudo/hostname.ts +156 -0
- package/src/pseudo/license.ts +134 -0
- package/src/pseudo/link.ts +30 -0
- package/src/pseudo/malware.ts +41 -0
- package/src/pseudo/minified.ts +9 -0
- package/src/pseudo/missing.ts +16 -0
- package/src/pseudo/native.ts +9 -0
- package/src/pseudo/network.ts +9 -0
- package/src/pseudo/obfuscated.ts +9 -0
- package/src/pseudo/optional.ts +18 -0
- package/src/pseudo/outdated.ts +305 -0
- package/src/pseudo/overridden.ts +20 -0
- package/src/pseudo/path.ts +146 -0
- package/src/pseudo/peer.ts +18 -0
- package/src/pseudo/prerelease.ts +47 -0
- package/src/pseudo/private.ts +19 -0
- package/src/pseudo/prod.ts +18 -0
- package/src/pseudo/published.ts +248 -0
- package/src/pseudo/registry.ts +29 -0
- package/src/pseudo/root.ts +19 -0
- package/src/pseudo/scanned.ts +20 -0
- package/src/pseudo/score.ts +186 -0
- package/src/pseudo/scripts.ts +54 -0
- package/src/pseudo/semver.ts +320 -0
- package/src/pseudo/severity.ts +231 -0
- package/src/pseudo/shell.ts +9 -0
- package/src/pseudo/shrinkwrap.ts +9 -0
- package/src/pseudo/spec.ts +131 -0
- package/src/pseudo/squat.ts +225 -0
- package/src/pseudo/suspicious.ts +9 -0
- package/src/pseudo/tracker.ts +9 -0
- package/src/pseudo/trivial.ts +9 -0
- package/src/pseudo/type.ts +26 -0
- package/src/pseudo/undesirable.ts +9 -0
- package/src/pseudo/unknown.ts +9 -0
- package/src/pseudo/unmaintained.ts +9 -0
- package/src/pseudo/unpopular.ts +9 -0
- package/src/pseudo/unstable.ts +9 -0
- package/src/pseudo/vuln.ts +273 -0
- package/src/pseudo/workspace.ts +22 -0
- package/src/pseudo.ts +413 -0
- package/src/types.ts +165 -0
- package/tap-snapshots/test/attribute.ts.test.cjs +304 -0
- package/tap-snapshots/test/combinator.ts.test.cjs +333 -0
- package/tap-snapshots/test/id.ts.test.cjs +118 -0
- package/tap-snapshots/test/parser.ts.test.cjs +344 -0
- package/tap-snapshots/test/pseudo/abandoned.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/attr.ts.test.cjs +161 -0
- package/tap-snapshots/test/pseudo/built.ts.test.cjs +70 -0
- package/tap-snapshots/test/pseudo/confused.ts.test.cjs +32 -0
- package/tap-snapshots/test/pseudo/cve.ts.test.cjs +49 -0
- package/tap-snapshots/test/pseudo/cwe.ts.test.cjs +49 -0
- package/tap-snapshots/test/pseudo/debug.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/deprecated.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/dist.ts.test.cjs +30 -0
- package/tap-snapshots/test/pseudo/dynamic.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/empty.ts.test.cjs +50 -0
- package/tap-snapshots/test/pseudo/entropic.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/env.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/eval.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/fs.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/helpers.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/hostname.ts.test.cjs +148 -0
- package/tap-snapshots/test/pseudo/license.ts.test.cjs +41 -0
- package/tap-snapshots/test/pseudo/link.ts.test.cjs +36 -0
- package/tap-snapshots/test/pseudo/malware.ts.test.cjs +22 -0
- package/tap-snapshots/test/pseudo/minified.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/missing.ts.test.cjs +30 -0
- package/tap-snapshots/test/pseudo/native.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/network.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/obfuscated.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/outdated.ts.test.cjs +130 -0
- package/tap-snapshots/test/pseudo/overridden.ts.test.cjs +75 -0
- package/tap-snapshots/test/pseudo/prerelease.ts.test.cjs +101 -0
- package/tap-snapshots/test/pseudo/private.ts.test.cjs +38 -0
- package/tap-snapshots/test/pseudo/published.ts.test.cjs +171 -0
- package/tap-snapshots/test/pseudo/registry.ts.test.cjs +68 -0
- package/tap-snapshots/test/pseudo/root.ts.test.cjs +24 -0
- package/tap-snapshots/test/pseudo/scanned.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/score.ts.test.cjs +239 -0
- package/tap-snapshots/test/pseudo/scripts.ts.test.cjs +61 -0
- package/tap-snapshots/test/pseudo/semver.ts.test.cjs +575 -0
- package/tap-snapshots/test/pseudo/severity.ts.test.cjs +95 -0
- package/tap-snapshots/test/pseudo/shell.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/shrinkwrap.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/spec.ts.test.cjs +148 -0
- package/tap-snapshots/test/pseudo/squat.ts.test.cjs +129 -0
- package/tap-snapshots/test/pseudo/suspicious.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/tracker.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/trivial.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/type.ts.test.cjs +51 -0
- package/tap-snapshots/test/pseudo/undesirable.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unknown.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unmaintained.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unpopular.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unstable.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/vuln.ts.test.cjs +208 -0
- package/tap-snapshots/test/pseudo/vulnerable.ts.test.cjs +20 -0
- package/tap-snapshots/test/pseudo/workspace.ts.test.cjs +19 -0
- package/tap-snapshots/test/pseudo.ts.test.cjs +1247 -0
- package/test/attribute.ts +266 -0
- package/test/combinator.ts +168 -0
- package/test/fixtures/graph.ts +999 -0
- package/test/fixtures/selector.ts +103 -0
- package/test/fixtures/types.ts +7 -0
- package/test/id.ts +102 -0
- package/test/index.ts +1000 -0
- package/test/parser.ts +117 -0
- package/test/pseudo/abandoned.ts +98 -0
- package/test/pseudo/attr.ts +298 -0
- package/test/pseudo/built.ts +214 -0
- package/test/pseudo/confused.ts +160 -0
- package/test/pseudo/cve.ts +249 -0
- package/test/pseudo/cwe.ts +255 -0
- package/test/pseudo/debug.ts +98 -0
- package/test/pseudo/deprecated.ts +98 -0
- package/test/pseudo/dev.ts +61 -0
- package/test/pseudo/diff.ts +417 -0
- package/test/pseudo/dist.ts +215 -0
- package/test/pseudo/dynamic.ts +98 -0
- package/test/pseudo/empty.ts +108 -0
- package/test/pseudo/entropic.ts +101 -0
- package/test/pseudo/env.ts +98 -0
- package/test/pseudo/eval.ts +98 -0
- package/test/pseudo/fs.ts +98 -0
- package/test/pseudo/helpers.ts +370 -0
- package/test/pseudo/host-context.ts +276 -0
- package/test/pseudo/hostname.ts +314 -0
- package/test/pseudo/license.ts +265 -0
- package/test/pseudo/link.ts +94 -0
- package/test/pseudo/malware.ts +184 -0
- package/test/pseudo/minified.ts +98 -0
- package/test/pseudo/missing.ts +111 -0
- package/test/pseudo/native.ts +98 -0
- package/test/pseudo/network.ts +98 -0
- package/test/pseudo/obfuscated.ts +98 -0
- package/test/pseudo/optional.ts +73 -0
- package/test/pseudo/outdated.ts +331 -0
- package/test/pseudo/overridden.ts +317 -0
- package/test/pseudo/path.ts +680 -0
- package/test/pseudo/peer.ts +101 -0
- package/test/pseudo/prerelease.ts +279 -0
- package/test/pseudo/private.ts +108 -0
- package/test/pseudo/prod.ts +61 -0
- package/test/pseudo/published.ts +557 -0
- package/test/pseudo/registry.ts +122 -0
- package/test/pseudo/root.ts +66 -0
- package/test/pseudo/scanned.ts +78 -0
- package/test/pseudo/score.ts +591 -0
- package/test/pseudo/scripts.ts +294 -0
- package/test/pseudo/semver.ts +822 -0
- package/test/pseudo/severity.ts +310 -0
- package/test/pseudo/shell.ts +98 -0
- package/test/pseudo/shrinkwrap.ts +98 -0
- package/test/pseudo/spec.ts +525 -0
- package/test/pseudo/squat.ts +565 -0
- package/test/pseudo/suspicious.ts +98 -0
- package/test/pseudo/tracker.ts +98 -0
- package/test/pseudo/trivial.ts +98 -0
- package/test/pseudo/type.ts +105 -0
- package/test/pseudo/undesirable.ts +98 -0
- package/test/pseudo/unknown.ts +98 -0
- package/test/pseudo/unmaintained.ts +98 -0
- package/test/pseudo/unpopular.ts +98 -0
- package/test/pseudo/unstable.ts +98 -0
- package/test/pseudo/vuln.ts +625 -0
- package/test/pseudo/vulnerable.ts +137 -0
- package/test/pseudo/workspace.ts +111 -0
- package/test/pseudo.ts +558 -0
- package/tsconfig.json +3 -0
- package/tsconfig.publish.json +19 -0
- package/tsconfig.publish.tsbuildinfo +1 -0
- package/typedoc.mjs +2 -0
- package/dist/pseudo/vulnerable.d.ts +0 -8
- package/dist/pseudo/vulnerable.js +0 -17
package/test/index.ts
ADDED
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import t from 'tap'
|
|
2
|
+
import { Query, walk } from '../src/index.ts'
|
|
3
|
+
import {
|
|
4
|
+
getCycleGraph,
|
|
5
|
+
getSimpleGraph,
|
|
6
|
+
getSingleWorkspaceGraph,
|
|
7
|
+
} from './fixtures/graph.ts'
|
|
8
|
+
import { copyGraphSelectionState } from './fixtures/selector.ts'
|
|
9
|
+
import { joinDepIDTuple } from '@vltpkg/dep-id'
|
|
10
|
+
import {
|
|
11
|
+
asPackageReportData,
|
|
12
|
+
asSecurityArchiveLike,
|
|
13
|
+
} from '@vltpkg/security-archive'
|
|
14
|
+
import type {
|
|
15
|
+
ParsedSelectorToken,
|
|
16
|
+
ParserState,
|
|
17
|
+
QueryResponse,
|
|
18
|
+
} from '../src/types.ts'
|
|
19
|
+
import type { PostcssNode } from '@vltpkg/dss-parser'
|
|
20
|
+
|
|
21
|
+
type TestCase = [string, string[]]
|
|
22
|
+
|
|
23
|
+
// Create a mock search options for tests
|
|
24
|
+
const mockSearchOptions = {
|
|
25
|
+
signal: { throwIfAborted: () => {} } as AbortSignal,
|
|
26
|
+
scopeIDs: [joinDepIDTuple(['file', '.'])],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const testBrokenState = (): ParserState => {
|
|
30
|
+
const graph = getSimpleGraph()
|
|
31
|
+
const initial = {
|
|
32
|
+
nodes: new Set(graph.nodes.values()),
|
|
33
|
+
edges: new Set(graph.edges.values()),
|
|
34
|
+
}
|
|
35
|
+
const state: ParserState = {
|
|
36
|
+
cancellable: async () => {},
|
|
37
|
+
collect: {
|
|
38
|
+
nodes: new Set(),
|
|
39
|
+
edges: new Set(),
|
|
40
|
+
},
|
|
41
|
+
comment: '',
|
|
42
|
+
current: { type: 'bork' } as unknown as PostcssNode,
|
|
43
|
+
initial: copyGraphSelectionState(initial),
|
|
44
|
+
partial: copyGraphSelectionState(initial),
|
|
45
|
+
importers: new Set(graph.importers),
|
|
46
|
+
hostContexts: undefined,
|
|
47
|
+
walk,
|
|
48
|
+
retries: 0,
|
|
49
|
+
scopeIDs: mockSearchOptions.scopeIDs,
|
|
50
|
+
securityArchive: undefined,
|
|
51
|
+
signal: mockSearchOptions.signal,
|
|
52
|
+
specificity: { idCounter: 0, commonCounter: 0 },
|
|
53
|
+
}
|
|
54
|
+
return state
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
t.test('simple graph', async t => {
|
|
58
|
+
const graph = getSimpleGraph()
|
|
59
|
+
const queryToExpected = new Set<TestCase>([
|
|
60
|
+
['', []], // should match no deps on missing query
|
|
61
|
+
['*', ['my-project', 'a', 'b', 'c', 'd', 'e', 'f', '@x/y']], // universal
|
|
62
|
+
['*', ['my-project', 'a', 'b', 'c', 'd', 'e', 'f', '@x/y']], // repeat, cache-hit
|
|
63
|
+
['* >*', ['a', 'b', 'e', '@x/y', 'c', 'd', 'f']], // dependencies
|
|
64
|
+
[':root', ['my-project']], // select :root
|
|
65
|
+
[':root > *', ['a', 'b', 'e', '@x/y']], // direct deps of :root
|
|
66
|
+
[':root > :root', []], // :root pseudo-selector can't depend on itself
|
|
67
|
+
[':dev', ['b', '@x/y']], // retrieve deps of dev type
|
|
68
|
+
[':root > :dev', ['b', '@x/y']], // mixed with a combinator
|
|
69
|
+
[':root > :dev[name=b]', ['b']], // specific node
|
|
70
|
+
[':root > [name=b]:dev', ['b']], // specific node backwards
|
|
71
|
+
[':root > *[name=b]:dev', ['b']], // specific node backwards with universal
|
|
72
|
+
[':root > :dev[name=b] ~ *', ['a', 'e', '@x/y']], // retrieves its siblings
|
|
73
|
+
[':root > :optional', []], // no direct optional dep
|
|
74
|
+
[':root :optional', ['f']], // optional descendent found
|
|
75
|
+
[':root *', ['a', 'b', 'e', '@x/y', 'c', 'd', 'f']], // all descendents from root
|
|
76
|
+
['[name=a]', ['a']], // select by name
|
|
77
|
+
[':root :has([name])', ['b', 'd']], // combined has example, filtering any descendent that has direct deps with a name
|
|
78
|
+
[':root > [name=a]', ['a']], // select children by name
|
|
79
|
+
[':root > [name=d] [name=b]', []], // no direct descendent with that name
|
|
80
|
+
[':root > [name=d] [name=e]', []], // descendent found by name
|
|
81
|
+
[':root [name=d] ~ [name=c]', ['c']], // fully qualified
|
|
82
|
+
[':root [name^=@x]', ['@x/y']], // attribute starts with value
|
|
83
|
+
['/* do something */ :root [name^=@x]', ['@x/y']], // support comments
|
|
84
|
+
[':root > *{&[name=a]}', ['a']], // support nesting
|
|
85
|
+
[':root > * { &[name=a] }', ['a']], // support nesting with spaces
|
|
86
|
+
[':root > * { &[name=a], &[name=b] }', ['a', 'b']], // support multiple nesting selectors
|
|
87
|
+
['[name=b], [name=c], [name=f]', ['b', 'c', 'f']], // select by name
|
|
88
|
+
['[name=d], [name=d] > *', ['d', 'e', 'f']], // select a package and its dependencies
|
|
89
|
+
[
|
|
90
|
+
':root > *, :prod, [name=a], :has(:dev, :optional)',
|
|
91
|
+
['a', 'b', 'e', '@x/y', 'c', 'd', 'my-project'],
|
|
92
|
+
],
|
|
93
|
+
['#a', ['a']], // identifier
|
|
94
|
+
['#a:v(1)', ['a']], // matches identifier + semver
|
|
95
|
+
['#a:v(2)', []], // fails to match identifier + semver
|
|
96
|
+
[':path("*")', ['my-project', '@x/y']], // path selector for all path-based nodes
|
|
97
|
+
[':path(".")', ['my-project']], // path selector for root
|
|
98
|
+
[':path("**/a/**")', []], // should not match since 'a' is not workspace or file type
|
|
99
|
+
[':root, #a, :foo', ['my-project', 'a']], // should be loose on multiple selectors
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
const query = new Query({
|
|
103
|
+
nodes: new Set(graph.nodes.values()),
|
|
104
|
+
edges: graph.edges,
|
|
105
|
+
importers: graph.importers,
|
|
106
|
+
securityArchive: undefined,
|
|
107
|
+
})
|
|
108
|
+
for (const [q, expected] of queryToExpected) {
|
|
109
|
+
t.strictSame(
|
|
110
|
+
(await query.search(q, mockSearchOptions)).nodes.map(
|
|
111
|
+
i => i.name,
|
|
112
|
+
),
|
|
113
|
+
expected,
|
|
114
|
+
`query > "${q}"`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await t.test('comments extraction', async t => {
|
|
119
|
+
const result = await query.search(
|
|
120
|
+
'/* comment one */ :root /* comment two */ > * /* comment three */',
|
|
121
|
+
mockSearchOptions,
|
|
122
|
+
)
|
|
123
|
+
t.strictSame(
|
|
124
|
+
result.comment,
|
|
125
|
+
'comment one',
|
|
126
|
+
'should extract the first comment from query',
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await t.test('comments extraction with no comments', async t => {
|
|
131
|
+
const result = await query.search(':root > *', mockSearchOptions)
|
|
132
|
+
t.strictSame(
|
|
133
|
+
result.comment,
|
|
134
|
+
'',
|
|
135
|
+
'should return empty string for query without comments',
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await t.test('comments cache hit', async t => {
|
|
140
|
+
const q = '/* cached comment */ :root'
|
|
141
|
+
await query.search(q, mockSearchOptions) // First call to populate cache
|
|
142
|
+
const result = await query.search(q, mockSearchOptions) // Second call should hit cache
|
|
143
|
+
t.strictSame(
|
|
144
|
+
result.comment,
|
|
145
|
+
'cached comment',
|
|
146
|
+
'should return comment for cached query results',
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
t.test('workspace', async t => {
|
|
152
|
+
const graph = getSingleWorkspaceGraph()
|
|
153
|
+
const queryToExpected = new Map<string, string[]>([
|
|
154
|
+
['', []], // should match no deps on missing query
|
|
155
|
+
['*', ['ws', 'w']], // universal
|
|
156
|
+
[':root', ['ws']], // select :root
|
|
157
|
+
[':root > *', []], // direct deps of :root
|
|
158
|
+
[':root > :root', []], // :root can't depend on itself
|
|
159
|
+
[':project#w', ['w']], // select by project + workspace name
|
|
160
|
+
['#w:project', ['w']], // should be interchangeable
|
|
161
|
+
['/* do something */ [name^=w]', ['ws', 'w']], // support comments
|
|
162
|
+
])
|
|
163
|
+
const query = new Query({
|
|
164
|
+
nodes: new Set(graph.nodes.values()),
|
|
165
|
+
edges: graph.edges,
|
|
166
|
+
importers: graph.importers,
|
|
167
|
+
securityArchive: undefined,
|
|
168
|
+
})
|
|
169
|
+
for (const [q, expected] of queryToExpected) {
|
|
170
|
+
t.strictSame(
|
|
171
|
+
(await query.search(q, mockSearchOptions)).nodes.map(
|
|
172
|
+
i => i.name,
|
|
173
|
+
),
|
|
174
|
+
expected,
|
|
175
|
+
`query > "${q}"`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await t.test('comments handling', async t => {
|
|
180
|
+
const result = await query.search(
|
|
181
|
+
'/* workspace comment */ :root',
|
|
182
|
+
mockSearchOptions,
|
|
183
|
+
)
|
|
184
|
+
t.strictSame(
|
|
185
|
+
result.comment,
|
|
186
|
+
'workspace comment',
|
|
187
|
+
'should extract comment from workspace query',
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
t.test('cycle', async t => {
|
|
193
|
+
const graph = getCycleGraph()
|
|
194
|
+
const queryToExpected = new Set<TestCase>([
|
|
195
|
+
['', []], // should match no deps on missing query
|
|
196
|
+
['*', ['cycle-project', 'a', 'b']], // universal
|
|
197
|
+
[':root', ['cycle-project']], // select :root
|
|
198
|
+
[':root > *', ['a']], // direct deps of :root
|
|
199
|
+
[':root > :root', []], // :root can depend on itself
|
|
200
|
+
['/* do something */ [name^=a]', ['a']], // support comments
|
|
201
|
+
[':root > :prod > *', ['b']], // mixed selectors
|
|
202
|
+
])
|
|
203
|
+
const query = new Query({
|
|
204
|
+
nodes: new Set(graph.nodes.values()),
|
|
205
|
+
edges: graph.edges,
|
|
206
|
+
importers: graph.importers,
|
|
207
|
+
securityArchive: undefined,
|
|
208
|
+
})
|
|
209
|
+
for (const [q, expected] of queryToExpected) {
|
|
210
|
+
t.strictSame(
|
|
211
|
+
(await query.search(q, mockSearchOptions)).nodes.map(
|
|
212
|
+
i => i.name,
|
|
213
|
+
),
|
|
214
|
+
expected,
|
|
215
|
+
`query > "${q}"`,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
t.test('insights', async t => {
|
|
221
|
+
const graph = getSimpleGraph()
|
|
222
|
+
const queryToExpected = new Set<TestCase>([
|
|
223
|
+
['*', ['my-project', 'a', 'b', 'c', 'd', 'e', 'f', '@x/y']], // universal
|
|
224
|
+
['*:severity(high)', ['e']], // universal with severity filter
|
|
225
|
+
['*:cve(CVE-2023-1234)', ['e']], // match cve by id
|
|
226
|
+
])
|
|
227
|
+
const query = new Query({
|
|
228
|
+
nodes: new Set(graph.nodes.values()),
|
|
229
|
+
edges: graph.edges,
|
|
230
|
+
importers: graph.importers,
|
|
231
|
+
securityArchive: asSecurityArchiveLike(
|
|
232
|
+
new Map([
|
|
233
|
+
[
|
|
234
|
+
joinDepIDTuple(['registry', '', 'e@1.0.0']),
|
|
235
|
+
asPackageReportData({
|
|
236
|
+
id: 'e@1.0.0',
|
|
237
|
+
author: [],
|
|
238
|
+
size: 0,
|
|
239
|
+
type: 'npm',
|
|
240
|
+
name: 'e',
|
|
241
|
+
version: '1.0.0',
|
|
242
|
+
license: 'MIT',
|
|
243
|
+
score: {
|
|
244
|
+
overall: 0.5,
|
|
245
|
+
security: 0.3,
|
|
246
|
+
maintenance: 0.7,
|
|
247
|
+
popularity: 0.6,
|
|
248
|
+
},
|
|
249
|
+
alerts: [
|
|
250
|
+
{
|
|
251
|
+
key: '12314320948130',
|
|
252
|
+
type: 'cve',
|
|
253
|
+
severity: 'high',
|
|
254
|
+
category: 'security',
|
|
255
|
+
props: {
|
|
256
|
+
cveId: 'CVE-2023-1234',
|
|
257
|
+
lastPublish: '2023-01-01',
|
|
258
|
+
cwes: [{ id: 'CWE-1234' }],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
]),
|
|
265
|
+
),
|
|
266
|
+
})
|
|
267
|
+
for (const [q, expected] of queryToExpected) {
|
|
268
|
+
const result = await query.search(q, mockSearchOptions)
|
|
269
|
+
t.strictSame(
|
|
270
|
+
result.nodes.map(i => i.name),
|
|
271
|
+
expected,
|
|
272
|
+
`query > "${q}"`,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Verify score values for node 'e'
|
|
276
|
+
const nodeE = result.nodes.find(n => n.name === 'e')
|
|
277
|
+
if (nodeE) {
|
|
278
|
+
t.strictSame(
|
|
279
|
+
nodeE.insights.score,
|
|
280
|
+
{
|
|
281
|
+
overall: 0.5,
|
|
282
|
+
security: 0.3,
|
|
283
|
+
maintenance: 0.7,
|
|
284
|
+
popularity: 0.6,
|
|
285
|
+
},
|
|
286
|
+
'node e should have correct score values',
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
t.test('bad selector type', async t => {
|
|
293
|
+
await t.rejects(
|
|
294
|
+
walk(testBrokenState()),
|
|
295
|
+
/Missing parser for query node: bork/,
|
|
296
|
+
'should throw a parser error',
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
t.test('bad selector type [loose mode]', async t => {
|
|
301
|
+
await t.resolves(
|
|
302
|
+
walk({ ...testBrokenState(), loose: true }),
|
|
303
|
+
'should resolve with no error',
|
|
304
|
+
)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
t.test('trying to use tag selectors', async t => {
|
|
308
|
+
const graph = getSimpleGraph()
|
|
309
|
+
await t.rejects(
|
|
310
|
+
new Query({
|
|
311
|
+
nodes: new Set(graph.nodes.values()),
|
|
312
|
+
edges: graph.edges,
|
|
313
|
+
importers: graph.importers,
|
|
314
|
+
securityArchive: undefined,
|
|
315
|
+
}).search('foo', mockSearchOptions),
|
|
316
|
+
/Unsupported selector/,
|
|
317
|
+
'should throw an unsupported selector error',
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
t.test('trying to use string selectors', async t => {
|
|
322
|
+
const graph = getSimpleGraph()
|
|
323
|
+
await t.rejects(
|
|
324
|
+
new Query({
|
|
325
|
+
nodes: new Set(graph.nodes.values()),
|
|
326
|
+
edges: graph.edges,
|
|
327
|
+
importers: graph.importers,
|
|
328
|
+
securityArchive: undefined,
|
|
329
|
+
}).search('"foo"', mockSearchOptions),
|
|
330
|
+
/Unsupported selector/,
|
|
331
|
+
'should throw an unsupported selector error',
|
|
332
|
+
)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
t.test('cancellable search', async t => {
|
|
336
|
+
const graph = getSingleWorkspaceGraph()
|
|
337
|
+
const query = new Query({
|
|
338
|
+
nodes: new Set(graph.nodes.values()),
|
|
339
|
+
edges: graph.edges,
|
|
340
|
+
importers: graph.importers,
|
|
341
|
+
securityArchive: undefined,
|
|
342
|
+
})
|
|
343
|
+
const ac = new AbortController()
|
|
344
|
+
const q = ':root > * > *'
|
|
345
|
+
await t.rejects(
|
|
346
|
+
new Promise((_, reject) => {
|
|
347
|
+
void query
|
|
348
|
+
.search(q, { signal: ac.signal })
|
|
349
|
+
.catch((err: unknown) => {
|
|
350
|
+
reject(err)
|
|
351
|
+
})
|
|
352
|
+
ac.abort(new Error('query aborted'))
|
|
353
|
+
// the set timeout bellow should never be called since
|
|
354
|
+
// the search should throw the abort error before
|
|
355
|
+
setTimeout(() => {
|
|
356
|
+
reject(new Error('ERR'))
|
|
357
|
+
}, 1000)
|
|
358
|
+
}),
|
|
359
|
+
/query aborted/,
|
|
360
|
+
'should reject with abort error',
|
|
361
|
+
)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
t.test('Query.hasSecuritySelectors', async t => {
|
|
365
|
+
t.ok(
|
|
366
|
+
Query.hasSecuritySelectors(':root > *:unmaintained'),
|
|
367
|
+
'should return true',
|
|
368
|
+
)
|
|
369
|
+
t.ok(
|
|
370
|
+
Query.hasSecuritySelectors(':root > *:has(:unmaintained)'),
|
|
371
|
+
'should return true',
|
|
372
|
+
)
|
|
373
|
+
t.ok(
|
|
374
|
+
Query.hasSecuritySelectors(':unmaintained'),
|
|
375
|
+
'should return true',
|
|
376
|
+
)
|
|
377
|
+
t.ok(
|
|
378
|
+
Query.hasSecuritySelectors(':vuln'),
|
|
379
|
+
'should return true for :vuln',
|
|
380
|
+
)
|
|
381
|
+
t.ok(
|
|
382
|
+
Query.hasSecuritySelectors(':vulnerable'),
|
|
383
|
+
'should return true for :vulnerable',
|
|
384
|
+
)
|
|
385
|
+
t.notOk(Query.hasSecuritySelectors(':foo'), 'should return false')
|
|
386
|
+
t.notOk(Query.hasSecuritySelectors(':has'), 'should return false')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
t.test('getQueryTokens', t => {
|
|
390
|
+
const normalizeParseOutput = (nodes: ParsedSelectorToken[]) => {
|
|
391
|
+
return nodes.map(node => {
|
|
392
|
+
return {
|
|
393
|
+
type: node.type,
|
|
394
|
+
token: node.token,
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
t.test('should getQueryTokens empty query', t => {
|
|
400
|
+
t.strictSame(normalizeParseOutput(Query.getQueryTokens('')), [])
|
|
401
|
+
t.end()
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
t.test('should getQueryTokens pseudo selector', t => {
|
|
405
|
+
t.strictSame(
|
|
406
|
+
normalizeParseOutput(Query.getQueryTokens(':root')),
|
|
407
|
+
[
|
|
408
|
+
{
|
|
409
|
+
type: 'pseudo',
|
|
410
|
+
token: ':root',
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
)
|
|
414
|
+
t.end()
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
t.test('should getQueryTokens attribute selector', t => {
|
|
418
|
+
t.test('with value', t => {
|
|
419
|
+
t.strictSame(
|
|
420
|
+
normalizeParseOutput(Query.getQueryTokens('[name="react"]')),
|
|
421
|
+
[
|
|
422
|
+
{
|
|
423
|
+
type: 'attribute',
|
|
424
|
+
token: '[name="react"]',
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
)
|
|
428
|
+
t.end()
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
t.test('with operator', t => {
|
|
432
|
+
t.strictSame(
|
|
433
|
+
normalizeParseOutput(Query.getQueryTokens('[name^="react"]')),
|
|
434
|
+
[
|
|
435
|
+
{
|
|
436
|
+
type: 'attribute',
|
|
437
|
+
token: '[name^="react"]',
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
)
|
|
441
|
+
t.end()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
t.test('without value', t => {
|
|
445
|
+
t.strictSame(
|
|
446
|
+
normalizeParseOutput(Query.getQueryTokens('[name]')),
|
|
447
|
+
[
|
|
448
|
+
{
|
|
449
|
+
type: 'attribute',
|
|
450
|
+
token: '[name]',
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
)
|
|
454
|
+
t.end()
|
|
455
|
+
})
|
|
456
|
+
t.end()
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
t.test('should getQueryTokens class selector', t => {
|
|
460
|
+
t.strictSame(
|
|
461
|
+
normalizeParseOutput(Query.getQueryTokens('.container')),
|
|
462
|
+
[
|
|
463
|
+
{
|
|
464
|
+
type: 'tag',
|
|
465
|
+
token: '.container',
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
)
|
|
469
|
+
t.end()
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
t.test('should getQueryTokens id selector', t => {
|
|
473
|
+
t.strictSame(
|
|
474
|
+
normalizeParseOutput(Query.getQueryTokens('#main')),
|
|
475
|
+
[
|
|
476
|
+
{
|
|
477
|
+
type: 'id',
|
|
478
|
+
token: '#main',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
t.test('should getQueryTokens scoped-name id selector', t => {
|
|
484
|
+
t.strictSame(
|
|
485
|
+
normalizeParseOutput(Query.getQueryTokens('#@scoped/name')),
|
|
486
|
+
[
|
|
487
|
+
{
|
|
488
|
+
type: 'id',
|
|
489
|
+
token: '#@scoped/name',
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
)
|
|
493
|
+
t.end()
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
t.test(
|
|
497
|
+
'should getQueryTokens dash-separated-scoped-name id selector',
|
|
498
|
+
t => {
|
|
499
|
+
t.strictSame(
|
|
500
|
+
normalizeParseOutput(
|
|
501
|
+
Query.getQueryTokens('#@scoped-org/name'),
|
|
502
|
+
),
|
|
503
|
+
[
|
|
504
|
+
{
|
|
505
|
+
type: 'id',
|
|
506
|
+
token: '#@scoped-org/name',
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
)
|
|
510
|
+
t.end()
|
|
511
|
+
},
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
t.test(
|
|
515
|
+
'should getQueryTokens dot-separated-scoped-name id selector',
|
|
516
|
+
t => {
|
|
517
|
+
t.strictSame(
|
|
518
|
+
normalizeParseOutput(
|
|
519
|
+
Query.getQueryTokens('#@scoped.org/name'),
|
|
520
|
+
),
|
|
521
|
+
[
|
|
522
|
+
{
|
|
523
|
+
type: 'id',
|
|
524
|
+
token: '#@scoped.org/name',
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
)
|
|
528
|
+
t.end()
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
t.end()
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
t.test(
|
|
535
|
+
'should getQueryTokens simple comma separated selector',
|
|
536
|
+
t => {
|
|
537
|
+
t.strictSame(
|
|
538
|
+
normalizeParseOutput(Query.getQueryTokens('#a, #b')),
|
|
539
|
+
[
|
|
540
|
+
{
|
|
541
|
+
type: 'id',
|
|
542
|
+
token: '#a',
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
type: 'selector',
|
|
546
|
+
token: ',',
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
type: 'id',
|
|
550
|
+
token: ' #b',
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
)
|
|
554
|
+
t.end()
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
t.test('should getQueryTokens tags', t => {
|
|
559
|
+
t.strictSame(
|
|
560
|
+
normalizeParseOutput(Query.getQueryTokens('name=test')),
|
|
561
|
+
[
|
|
562
|
+
{
|
|
563
|
+
type: 'tag',
|
|
564
|
+
token: 'name=test',
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
)
|
|
568
|
+
t.end()
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
t.test('should getQueryTokens strings', t => {
|
|
572
|
+
t.strictSame(
|
|
573
|
+
normalizeParseOutput(Query.getQueryTokens('name="test"')),
|
|
574
|
+
[
|
|
575
|
+
{
|
|
576
|
+
type: 'tag',
|
|
577
|
+
token: 'name=',
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
type: 'string',
|
|
581
|
+
token: '"test"',
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
)
|
|
585
|
+
t.end()
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
t.test('should getQueryTokens combinator', t => {
|
|
589
|
+
t.test('non-whitespace', t => {
|
|
590
|
+
t.strictSame(normalizeParseOutput(Query.getQueryTokens('>')), [
|
|
591
|
+
{
|
|
592
|
+
type: 'combinator',
|
|
593
|
+
token: '>',
|
|
594
|
+
},
|
|
595
|
+
])
|
|
596
|
+
t.end()
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
t.test('whitespace', t => {
|
|
600
|
+
t.strictSame(Query.getQueryTokens(' '), [])
|
|
601
|
+
t.end()
|
|
602
|
+
})
|
|
603
|
+
t.end()
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
t.test('should getQueryTokens complex selector', t => {
|
|
607
|
+
t.test('with nested selectors', t => {
|
|
608
|
+
t.strictSame(
|
|
609
|
+
normalizeParseOutput(
|
|
610
|
+
Query.getQueryTokens(':root > [name="react"] .container'),
|
|
611
|
+
),
|
|
612
|
+
[
|
|
613
|
+
{
|
|
614
|
+
type: 'pseudo',
|
|
615
|
+
token: ':root',
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
type: 'combinator',
|
|
619
|
+
token: ' > ',
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
type: 'attribute',
|
|
623
|
+
token: '[name="react"]',
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
type: 'combinator',
|
|
627
|
+
token: ' ',
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
type: 'tag',
|
|
631
|
+
token: '.container',
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
)
|
|
635
|
+
t.end()
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
t.test('with nested pseudo selectors', t => {
|
|
639
|
+
t.strictSame(
|
|
640
|
+
normalizeParseOutput(Query.getQueryTokens(':not(#react)')),
|
|
641
|
+
[
|
|
642
|
+
{
|
|
643
|
+
type: 'pseudo',
|
|
644
|
+
token: ':not(',
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
type: 'id',
|
|
648
|
+
token: '#react',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
type: 'pseudo',
|
|
652
|
+
token: ')',
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
)
|
|
656
|
+
t.test('with nested comma separated selectors', t => {
|
|
657
|
+
t.strictSame(
|
|
658
|
+
normalizeParseOutput(
|
|
659
|
+
Query.getQueryTokens(':not(#a, #b), :is(#c)'),
|
|
660
|
+
),
|
|
661
|
+
[
|
|
662
|
+
{
|
|
663
|
+
type: 'pseudo',
|
|
664
|
+
token: ':not(',
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
type: 'id',
|
|
668
|
+
token: '#a',
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
type: 'selector',
|
|
672
|
+
token: ',',
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
type: 'id',
|
|
676
|
+
token: ' #b',
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
type: 'pseudo',
|
|
680
|
+
token: ')',
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
type: 'selector',
|
|
684
|
+
token: ',',
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
type: 'pseudo',
|
|
688
|
+
token: ' :is(',
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
type: 'id',
|
|
692
|
+
token: '#c',
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
type: 'pseudo',
|
|
696
|
+
token: ')',
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
)
|
|
700
|
+
t.end()
|
|
701
|
+
})
|
|
702
|
+
t.end()
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
t.test('with malformed queries', t => {
|
|
706
|
+
t.test('should handle unclosed brackets', t => {
|
|
707
|
+
t.strictSame(
|
|
708
|
+
normalizeParseOutput(Query.getQueryTokens('[name="react"')),
|
|
709
|
+
[],
|
|
710
|
+
)
|
|
711
|
+
t.end()
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
t.test('should handle unclosed pseudo selectors', t => {
|
|
715
|
+
t.strictSame(
|
|
716
|
+
normalizeParseOutput(Query.getQueryTokens(':not(#react')),
|
|
717
|
+
[
|
|
718
|
+
{
|
|
719
|
+
type: 'pseudo',
|
|
720
|
+
token: ':not',
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
)
|
|
724
|
+
t.end()
|
|
725
|
+
})
|
|
726
|
+
t.end()
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
t.test('with multiple attributes', t => {
|
|
730
|
+
t.strictSame(
|
|
731
|
+
normalizeParseOutput(
|
|
732
|
+
Query.getQueryTokens('[name="react"][version="18"]'),
|
|
733
|
+
),
|
|
734
|
+
[
|
|
735
|
+
{
|
|
736
|
+
type: 'attribute',
|
|
737
|
+
token: '[name="react"]',
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
type: 'attribute',
|
|
741
|
+
token: '[version="18"]',
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
)
|
|
745
|
+
t.end()
|
|
746
|
+
})
|
|
747
|
+
t.end()
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
t.end()
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
t.test(':scope with custom scopeIDs', async t => {
|
|
754
|
+
const graph = getSimpleGraph()
|
|
755
|
+
const query = new Query({
|
|
756
|
+
nodes: new Set(graph.nodes.values()),
|
|
757
|
+
edges: graph.edges,
|
|
758
|
+
importers: graph.importers,
|
|
759
|
+
securityArchive: undefined,
|
|
760
|
+
})
|
|
761
|
+
const customScopeID = joinDepIDTuple(['registry', '', 'a@1.0.0'])
|
|
762
|
+
t.strictSame(
|
|
763
|
+
(
|
|
764
|
+
await query.search(':scope', {
|
|
765
|
+
...mockSearchOptions,
|
|
766
|
+
scopeIDs: [customScopeID],
|
|
767
|
+
})
|
|
768
|
+
).nodes.map(n => n.name),
|
|
769
|
+
['a'],
|
|
770
|
+
'should select only the node specified by scopeIDs',
|
|
771
|
+
)
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
t.test('specificity calculation', async t => {
|
|
775
|
+
const graph = getSimpleGraph()
|
|
776
|
+
const query = new Query({
|
|
777
|
+
nodes: new Set(graph.nodes.values()),
|
|
778
|
+
edges: graph.edges,
|
|
779
|
+
importers: graph.importers,
|
|
780
|
+
securityArchive: undefined,
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
const specificityTestCases = [
|
|
784
|
+
{
|
|
785
|
+
query: ':root',
|
|
786
|
+
idCounter: 0,
|
|
787
|
+
commonCounter: 1,
|
|
788
|
+
description: 'single pseudo-element',
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
query: ':root > #b',
|
|
792
|
+
idCounter: 1,
|
|
793
|
+
commonCounter: 1,
|
|
794
|
+
description: 'child combinator with id',
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
query: ':root #b',
|
|
798
|
+
idCounter: 1,
|
|
799
|
+
commonCounter: 1,
|
|
800
|
+
description: 'descendant combinator with id',
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
query: ':root ~ #b',
|
|
804
|
+
idCounter: 1,
|
|
805
|
+
commonCounter: 1,
|
|
806
|
+
description: 'sibling combinator with id',
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
query: ':root > #b > #c',
|
|
810
|
+
idCounter: 2,
|
|
811
|
+
commonCounter: 1,
|
|
812
|
+
description: 'multiple combinators with ids',
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
query: ':root > :prod',
|
|
816
|
+
idCounter: 0,
|
|
817
|
+
commonCounter: 2,
|
|
818
|
+
description: 'only pseudo classes',
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
query: ':root > :prod:dev',
|
|
822
|
+
idCounter: 0,
|
|
823
|
+
commonCounter: 3,
|
|
824
|
+
description: 'multiple pseudo classes',
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
query: ':root > :prod:dev:optional',
|
|
828
|
+
idCounter: 0,
|
|
829
|
+
commonCounter: 4,
|
|
830
|
+
description: 'multiple pseudo classes',
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
query: ':root > #missing',
|
|
834
|
+
idCounter: 1,
|
|
835
|
+
commonCounter: 1,
|
|
836
|
+
description: 'missing id still counts',
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
query: ':root > [name=b]',
|
|
840
|
+
idCounter: 0,
|
|
841
|
+
commonCounter: 2,
|
|
842
|
+
description: 'attribute selector',
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
query: ':root > [name=b][version=2.0.0]',
|
|
846
|
+
idCounter: 0,
|
|
847
|
+
commonCounter: 3,
|
|
848
|
+
description: 'multiple attribute selectors',
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
query: ':root > :not(:prod)',
|
|
852
|
+
idCounter: 0,
|
|
853
|
+
commonCounter: 1,
|
|
854
|
+
description: ':not does not affect specificity',
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
query: ':root > :is(:prod, :dev)',
|
|
858
|
+
idCounter: 0,
|
|
859
|
+
commonCounter: 1,
|
|
860
|
+
description: ':is does not affect specificity',
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
query: ':root > :has(:prod)',
|
|
864
|
+
idCounter: 0,
|
|
865
|
+
commonCounter: 1,
|
|
866
|
+
description: ':has does not affect specificity',
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
query: '#b:prod',
|
|
870
|
+
idCounter: 1,
|
|
871
|
+
commonCounter: 1,
|
|
872
|
+
description: 'id with pseudo class',
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
query: '#b:prod[name=b]',
|
|
876
|
+
idCounter: 1,
|
|
877
|
+
commonCounter: 2,
|
|
878
|
+
description: 'id with pseudo class and attribute',
|
|
879
|
+
},
|
|
880
|
+
]
|
|
881
|
+
|
|
882
|
+
for (const {
|
|
883
|
+
query: queryStr,
|
|
884
|
+
idCounter,
|
|
885
|
+
commonCounter,
|
|
886
|
+
description,
|
|
887
|
+
} of specificityTestCases) {
|
|
888
|
+
const result = await query.search(queryStr, mockSearchOptions)
|
|
889
|
+
t.strictSame(
|
|
890
|
+
{
|
|
891
|
+
idCounter: result.specificity.idCounter,
|
|
892
|
+
commonCounter: result.specificity.commonCounter,
|
|
893
|
+
},
|
|
894
|
+
{ idCounter, commonCounter },
|
|
895
|
+
`specificity for "${queryStr}" - ${description}`,
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
t.test('specificitySort', async t => {
|
|
901
|
+
// Create test QueryResponse objects with different specificity values
|
|
902
|
+
const responses: QueryResponse[] = [
|
|
903
|
+
{
|
|
904
|
+
edges: [],
|
|
905
|
+
nodes: [],
|
|
906
|
+
importers: [],
|
|
907
|
+
comment: 'First response (id:0, common:1)',
|
|
908
|
+
specificity: { idCounter: 0, commonCounter: 1 },
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
edges: [],
|
|
912
|
+
nodes: [],
|
|
913
|
+
importers: [],
|
|
914
|
+
comment: 'Second response (id:2, common:0)',
|
|
915
|
+
specificity: { idCounter: 2, commonCounter: 0 },
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
edges: [],
|
|
919
|
+
nodes: [],
|
|
920
|
+
importers: [],
|
|
921
|
+
comment: 'Third response (id:1, common:3)',
|
|
922
|
+
specificity: { idCounter: 1, commonCounter: 3 },
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
edges: [],
|
|
926
|
+
nodes: [],
|
|
927
|
+
importers: [],
|
|
928
|
+
comment: 'Fourth response (id:1, common:2)',
|
|
929
|
+
specificity: { idCounter: 1, commonCounter: 2 },
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
edges: [],
|
|
933
|
+
nodes: [],
|
|
934
|
+
importers: [],
|
|
935
|
+
comment: 'Fifth response (id:0, common:0)',
|
|
936
|
+
specificity: { idCounter: 0, commonCounter: 0 },
|
|
937
|
+
},
|
|
938
|
+
]
|
|
939
|
+
|
|
940
|
+
const sortedResponses = Query.specificitySort(responses)
|
|
941
|
+
|
|
942
|
+
// Check that the order is correct based on specificity rules
|
|
943
|
+
t.strictSame(
|
|
944
|
+
sortedResponses.map(r => r.comment),
|
|
945
|
+
[
|
|
946
|
+
'Second response (id:2, common:0)', // Highest idCounter (2)
|
|
947
|
+
'Third response (id:1, common:3)', // Next idCounter (1) with higher commonCounter (3)
|
|
948
|
+
'Fourth response (id:1, common:2)', // Same idCounter (1) with lower commonCounter (2)
|
|
949
|
+
'First response (id:0, common:1)', // Lowest idCounter (0) with higher commonCounter (1)
|
|
950
|
+
'Fifth response (id:0, common:0)', // Lowest idCounter (0) with lowest commonCounter (0)
|
|
951
|
+
],
|
|
952
|
+
'should sort responses by specificity with higher values first',
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
// Test that the original array is not modified
|
|
956
|
+
t.notSame(
|
|
957
|
+
responses.map(r => r.comment),
|
|
958
|
+
sortedResponses.map(r => r.comment),
|
|
959
|
+
'should not modify the original array',
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
// Test that the sort is stable (preserves original order for equal specificity)
|
|
963
|
+
const equalSpecificityResponses: QueryResponse[] = [
|
|
964
|
+
{
|
|
965
|
+
edges: [],
|
|
966
|
+
nodes: [],
|
|
967
|
+
importers: [],
|
|
968
|
+
comment: 'First equal response',
|
|
969
|
+
specificity: { idCounter: 1, commonCounter: 1 },
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
edges: [],
|
|
973
|
+
nodes: [],
|
|
974
|
+
importers: [],
|
|
975
|
+
comment: 'Second equal response',
|
|
976
|
+
specificity: { idCounter: 1, commonCounter: 1 },
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
edges: [],
|
|
980
|
+
nodes: [],
|
|
981
|
+
importers: [],
|
|
982
|
+
comment: 'Third equal response',
|
|
983
|
+
specificity: { idCounter: 1, commonCounter: 1 },
|
|
984
|
+
},
|
|
985
|
+
]
|
|
986
|
+
|
|
987
|
+
const sortedEqualResponses = Query.specificitySort(
|
|
988
|
+
equalSpecificityResponses,
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
t.strictSame(
|
|
992
|
+
sortedEqualResponses.map(r => r.comment),
|
|
993
|
+
[
|
|
994
|
+
'First equal response',
|
|
995
|
+
'Second equal response',
|
|
996
|
+
'Third equal response',
|
|
997
|
+
],
|
|
998
|
+
'should preserve original order for equal specificity values',
|
|
999
|
+
)
|
|
1000
|
+
})
|