@amritk/lint 0.4.8 → 0.5.0
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/AI.md +73 -4
- package/README.md +65 -14
- package/dist/core/filter-expression.js +17 -4
- package/dist/core/filter.js +82 -35
- package/dist/core/glob.js +25 -31
- package/dist/core/jsonpath.d.ts +9 -1
- package/dist/core/jsonpath.js +74 -16
- package/dist/core/lint.d.ts +8 -1
- package/dist/core/lint.js +4 -18
- package/dist/core/order.d.ts +30 -0
- package/dist/core/order.js +36 -0
- package/dist/core/pointers.d.ts +3 -2
- package/dist/core/pointers.js +27 -18
- package/dist/core/ruleset.js +62 -20
- package/dist/core/runner.js +16 -23
- package/dist/core/severity.d.ts +11 -0
- package/dist/core/severity.js +13 -0
- package/dist/fix/apply.js +6 -1
- package/dist/fix/plugin.js +2 -1
- package/dist/functions/or.js +1 -1
- package/dist/functions/ref-index.d.ts +15 -0
- package/dist/functions/ref-index.js +33 -0
- package/dist/functions/schema.d.ts +15 -0
- package/dist/functions/schema.js +2 -0
- package/dist/functions/typed-enum.js +2 -1
- package/dist/functions/unreferenced-reusable-object.js +3 -20
- package/dist/functions/xor.js +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +24 -93
- package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
- package/dist/rules/asyncapi/asyncapi.js +563 -0
- package/dist/rules/asyncapi/formats.d.ts +23 -0
- package/dist/rules/asyncapi/formats.js +49 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
- package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
- package/dist/rules/asyncapi/functions/helpers.js +91 -0
- package/dist/rules/asyncapi/functions/index.d.ts +15 -0
- package/dist/rules/asyncapi/functions/index.js +53 -0
- package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
- package/dist/rules/asyncapi/functions/pointer.js +11 -0
- package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
- package/dist/rules/asyncapi/functions/schema-format.js +7 -0
- package/dist/rules/asyncapi/index.d.ts +33 -0
- package/dist/rules/asyncapi/index.js +60 -0
- package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas20.js +4 -0
- package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas21.js +4 -0
- package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas22.js +4 -0
- package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas23.js +4 -0
- package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas24.js +4 -0
- package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas25.js +4 -0
- package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas26.js +4 -0
- package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas30.js +4 -0
- package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
- package/dist/rules/asyncapi/schemas/index.js +45 -0
- package/dist/rules/openapi/fixers.d.ts +6 -2
- package/dist/rules/openapi/fixers.js +2 -0
- package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
- package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
- package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
- package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
- package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
- package/dist/rules/openapi/index.d.ts +9 -2
- package/dist/rules/openapi/index.js +15 -70
- package/dist/rules/openapi/schemas/index.js +5 -1
- package/dist/rules/shared/helpers.d.ts +2 -0
- package/dist/rules/shared/helpers.js +4 -0
- package/dist/rules/shared/server-variables.d.ts +27 -0
- package/dist/rules/shared/server-variables.js +55 -0
- package/dist/rules/shared/tags-unique.d.ts +7 -0
- package/dist/rules/shared/tags-unique.js +19 -0
- package/dist/ruleset-files.d.ts +47 -0
- package/dist/ruleset-files.js +92 -0
- package/package.json +8 -4
package/AI.md
CHANGED
|
@@ -20,6 +20,43 @@ const ruleset = {
|
|
|
20
20
|
const findings = await lintDocument('version: 1\n', { ruleset, source: 'service.yaml' })
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## Presets: OpenAPI and AsyncAPI
|
|
24
|
+
|
|
25
|
+
The core engine knows no spec. Two ready-made rulesets live in subpaths — build
|
|
26
|
+
one and hand the **result** to `lintDocument`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { lintDocument } from '@amritk/lint'
|
|
30
|
+
import { createAsyncApiRuleset } from '@amritk/lint/rules/asyncapi'
|
|
31
|
+
import { resolveRefs } from '@amritk/resolve-refs' // any resolver will do
|
|
32
|
+
|
|
33
|
+
// Recommended rules only: 48 of 56. For all 56, pass the `all` modifier:
|
|
34
|
+
// createAsyncApiRuleset({ extends: [['asyncapi', 'all']] })
|
|
35
|
+
const ruleset = createAsyncApiRuleset()
|
|
36
|
+
|
|
37
|
+
const findings = await lintDocument(source, {
|
|
38
|
+
ruleset,
|
|
39
|
+
source: 'asyncapi.yaml',
|
|
40
|
+
// Optional, and load-bearing — see the gotcha below.
|
|
41
|
+
resolve: (document) => ({ resolved: resolveRefs(document.data).resolved }),
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`createOpenApiRuleset()` is the same shape (66 rules, `oas` / `loupe:oas` /
|
|
46
|
+
`spectral:oas` in `extends`) and additionally ships `oasFixers` for
|
|
47
|
+
`fixDocument`. **There are no AsyncAPI fixers**, so `fixDocument` with an
|
|
48
|
+
AsyncAPI ruleset changes nothing.
|
|
49
|
+
|
|
50
|
+
Rule names match Spectral's, so a `.spectral.yml` that re-severities individual
|
|
51
|
+
rules ports over. Override by name in your own definition:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
createAsyncApiRuleset({
|
|
55
|
+
extends: [['asyncapi', 'all']],
|
|
56
|
+
rules: { 'asyncapi-info-license': 'off', 'asyncapi-tag-description': 'error' },
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
23
60
|
## Gotchas — where agents fail
|
|
24
61
|
|
|
25
62
|
1. **Two `severity` vocabularies.** In a **ruleset** you author strings
|
|
@@ -34,8 +71,15 @@ const findings = await lintDocument('version: 1\n', { ruleset, source: 'service.
|
|
|
34
71
|
4. **`extends` targets are file paths or npm packages only** — there are no
|
|
35
72
|
named built-in rulesets in core. String `extends` resolve relative to
|
|
36
73
|
`rulesetBasePath` (or the ruleset file's own directory).
|
|
37
|
-
5. **OpenAPI support
|
|
38
|
-
(`createOpenApiRuleset`, `oas`, `oasFixers`, …)
|
|
74
|
+
5. **OpenAPI and AsyncAPI support are separate subpaths**,
|
|
75
|
+
`@amritk/lint/rules/openapi` (`createOpenApiRuleset`, `oas`, `oasFixers`, …)
|
|
76
|
+
and `@amritk/lint/rules/asyncapi` (`createAsyncApiRuleset`, `asyncapi`, …) —
|
|
77
|
+
not the package root. Build one with `createOpenApiRuleset()` /
|
|
78
|
+
`createAsyncApiRuleset()` and hand the *result* to `lintDocument` /
|
|
79
|
+
`fixDocument` as `ruleset`. Passing the `oas` or `asyncapi` definition as data
|
|
80
|
+
instead silently produces nothing: its custom functions are unknown at the
|
|
81
|
+
package root, and its `formats` gate matches nothing without that spec's
|
|
82
|
+
detectors.
|
|
39
83
|
6. **A ruleset is privileged; a document is not.** Linted documents cannot
|
|
40
84
|
execute anything, and `[?(...)]` filters in a `given` are parsed and
|
|
41
85
|
interpreted rather than evaluated as JavaScript. But `extends` follows any
|
|
@@ -47,10 +91,34 @@ const findings = await lintDocument('version: 1\n', { ruleset, source: 'service.
|
|
|
47
91
|
7. **`createRuleset` is memoized** per `(definition object, basePath,
|
|
48
92
|
restrictTo)`. Mutating a definition you already passed in will not rebuild the
|
|
49
93
|
ruleset — pass a fresh object instead.
|
|
94
|
+
8. **Most AsyncAPI rules are gated per major** (11 of the 56 apply to both).
|
|
95
|
+
The 3.x-only rules are named with an `asyncapi-3-` prefix
|
|
96
|
+
(`asyncapi-3-operation-description`) and the 2.x ones are not, because 3.0
|
|
97
|
+
moved operations to the top level and tags under `info`. Re-severitying
|
|
98
|
+
`asyncapi-operation-description` does nothing to a 3.0 document. The
|
|
99
|
+
structural rules pick their meta-schema from the `asyncapi` field, and a
|
|
100
|
+
version with no bundled schema (a future `2.7`, say) reports nothing rather
|
|
101
|
+
than being judged against a neighbouring version's.
|
|
102
|
+
9. **Without a `resolve` hook, nothing behind a `$ref` is checked** — quietly.
|
|
103
|
+
The rules that validate schema *content* (payloads, headers, message
|
|
104
|
+
examples) need the dereferenced tree; given `payload: { $ref: … }` and no
|
|
105
|
+
resolver they see an opaque reference and stay silent rather than guess. You
|
|
106
|
+
get findings, just fewer of them, with no warning that a resolver would have
|
|
107
|
+
found more. Rules that read what the author wrote (addresses, server
|
|
108
|
+
variables, tag names) run unresolved by design and are unaffected.
|
|
109
|
+
|
|
110
|
+
The flip side: with a resolver, a rule that validates content matches a
|
|
111
|
+
reusable `components` definition once per `$ref` reaching it. Each match is
|
|
112
|
+
reported at the definition, so the copies are identical and only the first is
|
|
113
|
+
returned — you do not need to de-duplicate a report yourself. Two findings
|
|
114
|
+
that share a `line:column` are genuinely two problems (the classic case:
|
|
115
|
+
`info.contact` missing `name`, `url` and `email` all carry the `contact`
|
|
116
|
+
object's range), so key on `path` if you need to tell them apart.
|
|
50
117
|
|
|
51
118
|
## Exports
|
|
52
119
|
|
|
53
|
-
- `lintDocument(input, options?)` → findings only.
|
|
120
|
+
- `lintDocument(input, options?)` → findings only. `options.ruleset` is either a
|
|
121
|
+
definition to build or an already-built `Ruleset`.
|
|
54
122
|
- `lintDocumentWithResult(input, options?)` → `{ diagnostics, output?, pluginData }`.
|
|
55
123
|
- `fixDocument(input, options?)` → `{ output, fixed, applied, remaining, converged, passes }`
|
|
56
124
|
(needs `fixers`; `converged: false` means the 10-pass cap was hit with the
|
|
@@ -64,6 +132,7 @@ const findings = await lintDocument('version: 1\n', { ruleset, source: 'service.
|
|
|
64
132
|
| Import | Purpose |
|
|
65
133
|
|---|---|
|
|
66
134
|
| `@amritk/lint` | core engine, `lintDocument`/`fixDocument`, built-in functions |
|
|
67
|
-
| `@amritk/lint/rules/openapi` |
|
|
135
|
+
| `@amritk/lint/rules/openapi` | OpenAPI 2.0/3.0/3.1/3.2 preset — 66 rules, plus `oasFixers` |
|
|
136
|
+
| `@amritk/lint/rules/asyncapi` | AsyncAPI 2.0–2.6 / 3.0 preset — 56 rules, no fixers |
|
|
68
137
|
|
|
69
138
|
Install: `bun add @amritk/lint`.
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
It is **format-agnostic**: the core engine ships no built-in ruleset and knows nothing about OpenAPI or any other schema — you bring the rules. This is JSON/YAML style-guide linting with JSON Schema and custom rules at its core.
|
|
23
23
|
|
|
24
|
-
For OpenAPI specifically, the `@amritk/lint/rules/openapi`
|
|
24
|
+
For OpenAPI and AsyncAPI specifically, the `@amritk/lint/rules/openapi` and `@amritk/lint/rules/asyncapi` subpaths ship ready-made presets on top of that engine — see [OpenAPI ruleset](#openapi-ruleset) and [AsyncAPI ruleset](#asyncapi-ruleset) below.
|
|
25
25
|
|
|
26
26
|
The CLI lives in the [`mjst`](../cli) binary as `mjst lint`; this package is the programmatic library behind it.
|
|
27
27
|
|
|
@@ -123,7 +123,9 @@ await lintDocument(source, {
|
|
|
123
123
|
})
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
`restrictTo` is off by default and narrows *which* files a ruleset can name. It is not a sandbox: a `.js` file inside the permitted root still runs with full privileges.
|
|
126
|
+
`restrictTo` is off by default and narrows *which* files a ruleset can name. It is not a sandbox: a `.js` file inside the permitted root still runs with full privileges. The OpenAPI preset takes the same option — `createOpenApiRuleset(definition, basePath, { restrictTo })`.
|
|
127
|
+
|
|
128
|
+
One thing `restrictTo` does not cover: a regular expression a ruleset writes — in `pattern`'s `match`/`notMatch`, or as a literal inside a `[?(...)]` filter — is compiled and run against text from the document. An ambiguous pattern (nested quantifiers over overlapping character classes) can backtrack catastrophically on an input crafted to trigger it, so a hostile *document* can hang the linter through a regex the *ruleset* provided. The built-in rules avoid such patterns deliberately (see the note in `casing`); keep the same discipline in rules you write, and prefer anchored patterns with unambiguous alternatives.
|
|
127
129
|
|
|
128
130
|
### Auto-fix
|
|
129
131
|
|
|
@@ -147,11 +149,23 @@ const { output, applied, remaining } = await fixDocument('host: api.example.com/
|
|
|
147
149
|
|
|
148
150
|
The engine ships no built-in fixers (rule codes are yours to define), so the default registry is empty and `fixDocument` is a no-op until you supply one.
|
|
149
151
|
|
|
152
|
+
`options.ruleset` takes either a definition (built here with the built-in functions) or a `Ruleset` you built yourself. Pass a built one whenever the rules need functions or format detectors the package root does not have — an OpenAPI preset, for instance, whose rules would otherwise all be skipped as unknown-function/unmatched-format:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { fixDocument } from '@amritk/lint'
|
|
156
|
+
import { createOpenApiRuleset, oasFixers } from '@amritk/lint/rules/openapi'
|
|
157
|
+
|
|
158
|
+
const { output, applied } = await fixDocument(source, {
|
|
159
|
+
ruleset: createOpenApiRuleset(),
|
|
160
|
+
fixers: oasFixers,
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
150
164
|
Fixing runs to a fixpoint, capped at 10 passes. The result reports how that ended: **`converged`** is `false` when the cap was hit while the document was still changing (usually two fixers undoing each other), and **`passes`** counts the passes that changed something. `applied` is de-duplicated by rule code and path, so a report can safely say "fixed N problems".
|
|
151
165
|
|
|
152
166
|
### Rendering findings
|
|
153
167
|
|
|
154
|
-
`lintDocument` returns structured `IDiagnostic[]` — each with a `code`, `message`, `path`, `severity`, `source`, and a zero-based `range`. **Rendering is the caller's job**: print them, serialize them to JSON, or map them to whatever your editor or CI consumes. The linter deliberately ships no output "formatter" layer (that is not the same thing as `prettier`/`biome format`, which reformat source).
|
|
168
|
+
`lintDocument` returns structured `IDiagnostic[]` — each with a `code`, `message`, `path`, `severity`, `source`, and a zero-based `range`. `source`, `path` and `range` all describe the same node in the same document: the one the author wrote. A rule that runs against the dereferenced tree matches inside an inlined `$ref` target, and its finding is reported at the declaration that target came from, not at the `$ref` site. Two `$ref`s to the same mistake therefore produce one finding, not two. **Rendering is the caller's job**: print them, serialize them to JSON, or map them to whatever your editor or CI consumes. The linter deliberately ships no output "formatter" layer (that is not the same thing as `prettier`/`biome format`, which reformat source).
|
|
155
169
|
|
|
156
170
|
```ts
|
|
157
171
|
const findings = await lintDocument(source, { ruleset, source: 'doc.yaml' })
|
|
@@ -209,7 +223,7 @@ Anything outside that grammar is a ruleset error (`createRuleset` throws and nam
|
|
|
209
223
|
|
|
210
224
|
| Export | What it does |
|
|
211
225
|
| --- | --- |
|
|
212
|
-
| `lintDocument(input, options?)` | Parse `input` and lint it against `options.ruleset
|
|
226
|
+
| `lintDocument(input, options?)` | Parse `input` and lint it against `options.ruleset` — a definition, or a `Ruleset` you already built; returns `IDiagnostic[]`. |
|
|
213
227
|
| `lintDocumentWithResult(input, options?)` | Like `lintDocument`, but returns `{ diagnostics, output?, pluginData }` (including any plugin's rewritten `output`). |
|
|
214
228
|
| `fixDocument(input, options?)` | Lint and apply `options.fixers` to a fixpoint; returns `{ output, fixed, applied, remaining, converged, passes }`. |
|
|
215
229
|
| `createRuleset(definition?, basePath?, options?)` | Normalize a ruleset definition into a runnable `Ruleset`, layering the built-in functions and resolving `extends`. Memoized per `(definition object, basePath, restrictTo)` — treat a definition you have passed in as frozen. |
|
|
@@ -233,16 +247,16 @@ const ruleset = createOpenApiRuleset()
|
|
|
233
247
|
const findings = await lint(spec, { ruleset })
|
|
234
248
|
```
|
|
235
249
|
|
|
236
|
-
`createOpenApiRuleset(definition?, basePath?)` builds a runnable `Ruleset` with the OpenAPI functions and format detectors layered over the built-ins, and with `extends` resolution that understands the `oas` / `loupe:oas` / `spectral:oas` names (the last two accepted so existing Spectral-style rulesets extend unchanged). Enable every rule with `createOpenApiRuleset({ extends: [['oas', 'all']] })`, or pass your own definition to override severities, add rules, or point `extends` at a file/npm package.
|
|
250
|
+
`createOpenApiRuleset(definition?, basePath?, options?)` builds a runnable `Ruleset` with the OpenAPI functions and format detectors layered over the built-ins, and with `extends` resolution that understands the `oas` / `loupe:oas` / `spectral:oas` names (the last two accepted so existing Spectral-style rulesets extend unchanged). Enable every rule with `createOpenApiRuleset({ extends: [['oas', 'all']] })`, or pass your own definition to override severities, add rules, or point `extends` at a file/npm package. `options.restrictTo` applies the same [trust boundary](#trust-boundary--who-may-write-a-ruleset) the package root's `restrictTo` does.
|
|
237
251
|
|
|
238
252
|
| Export | What it does |
|
|
239
253
|
| --- | --- |
|
|
240
|
-
| `createOpenApiRuleset(definition?, basePath?)` | Build a runnable OpenAPI `Ruleset` (functions + formats + `extends` resolution). |
|
|
241
|
-
| `resolveOpenApiRuleset(name, basePath?)` | Resolve an `extends` reference, including the `oas` / `loupe:oas` / `spectral:oas` names. |
|
|
254
|
+
| `createOpenApiRuleset(definition?, basePath?, options?)` | Build a runnable OpenAPI `Ruleset` (functions + formats + `extends` resolution). |
|
|
255
|
+
| `resolveOpenApiRuleset(name, basePath?, options?)` | Resolve an `extends` reference, including the `oas` / `loupe:oas` / `spectral:oas` names. |
|
|
242
256
|
| `oas` | The built-in OpenAPI ruleset definition. |
|
|
243
257
|
| `oasFunctions` / `allFunctions` | The OpenAPI-specific functions; `allFunctions` = built-ins + OpenAPI. |
|
|
244
258
|
| `oasFormats` | OpenAPI version detectors (`oas2`, `oas3`, `oas3.0`, `oas3.1`, `oas3.2`). |
|
|
245
|
-
| `oasFixers` | Auto-fixers for the mechanically-repairable OpenAPI rules (pass to `fixDocument`). |
|
|
259
|
+
| `oasFixers` | Auto-fixers for the mechanically-repairable OpenAPI rules (pass to `fixDocument` alongside a built OpenAPI ruleset). |
|
|
246
260
|
| `loadOasSchema(version)` | Lazily load one OpenAPI version's official structural meta-schema (`'2.0'` / `'3.0'` / `'3.1'` / `'3.2'`), vendored as raw `.json` from `spec.openapis.org` (3.0/3.1/3.2 verbatim; 2.0 with its external draft-04 metaschema refs inlined). See [`schemas/README.md`](./src/rules/openapi/schemas/README.md). |
|
|
247
261
|
|
|
248
262
|
The structural rules validate against the **official `spec.openapis.org` meta-schemas, vendored as raw `.json`** ([`schemas/`](./src/rules/openapi/schemas/)). 3.0/3.1/3.2 are byte-for-byte verbatim; only 2.0 differs (its external draft-04 metaschema refs are inlined, since the offline interpreter never fetches remote refs). OpenAPI 3.1/3.2 express Schema Objects as JSON Schema 2020-12 via a local `$dynamicRef`/`$dynamicAnchor`, which `@amritk/runtime-validators` resolves natively — so the whole document envelope is validated against the official schema with no bundling or dialect engine, while Schema Object internals stay permissive.
|
|
@@ -251,21 +265,58 @@ The structural rules validate against the **official `spec.openapis.org` meta-sc
|
|
|
251
265
|
|
|
252
266
|
---
|
|
253
267
|
|
|
268
|
+
## AsyncAPI ruleset
|
|
269
|
+
|
|
270
|
+
The same treatment for event-driven APIs. The **`@amritk/lint/rules/asyncapi`** subpath ships a preset covering **AsyncAPI 2.0–2.6 and 3.0**, layered on the same engine and adding **no dependencies**.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { lint } from '@amritk/lint'
|
|
274
|
+
import { createAsyncApiRuleset } from '@amritk/lint/rules/asyncapi'
|
|
275
|
+
|
|
276
|
+
// Defaults to `extends: [asyncapi]` (recommended rules only, like `spectral:asyncapi`).
|
|
277
|
+
const ruleset = createAsyncApiRuleset()
|
|
278
|
+
const findings = await lint(document, { ruleset })
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`createAsyncApiRuleset(definition?, basePath?, options?)` mirrors its OpenAPI counterpart: functions and format detectors layered over the built-ins, and `extends` resolution that understands the `asyncapi` / `loupe:asyncapi` / `spectral:asyncapi` names (the last two so existing Spectral-style rulesets extend unchanged). Enable every rule with `createAsyncApiRuleset({ extends: [['asyncapi', 'all']] })`. `options.restrictTo` applies the same [trust boundary](#trust-boundary--who-may-write-a-ruleset).
|
|
282
|
+
|
|
283
|
+
| Export | What it does |
|
|
284
|
+
| --- | --- |
|
|
285
|
+
| `createAsyncApiRuleset(definition?, basePath?, options?)` | Build a runnable AsyncAPI `Ruleset` (functions + formats + `extends` resolution). |
|
|
286
|
+
| `resolveAsyncApiRuleset(name, basePath?, options?)` | Resolve an `extends` reference, including the `asyncapi` / `loupe:asyncapi` / `spectral:asyncapi` names. |
|
|
287
|
+
| `asyncapi` | The built-in AsyncAPI ruleset definition. |
|
|
288
|
+
| `aasFunctions` / `allFunctions` | The AsyncAPI-specific functions; `allFunctions` = built-ins + AsyncAPI. |
|
|
289
|
+
| `aasFormats` | AsyncAPI version detectors (`aas2`, `aas2.0`–`aas2.6`, `aas3`, `aas3.0`). |
|
|
290
|
+
| `loadAsyncApiSchema(version)` | Lazily load one version's official structural meta-schema (`'2.0'`–`'2.6'`, `'3.0'`). See [`schemas/README.md`](./src/rules/asyncapi/schemas/README.md). |
|
|
291
|
+
| `ASYNCAPI_VERSIONS` / `LATEST_ASYNCAPI_VERSION` / `asyncApiSchemaVersion(declared)` | The bundled versions, the newest one, and the mapping from a document's `asyncapi` field to the minor whose schema covers it (`'2.6.4'` → `'2.6'`; an unbundled minor → `undefined`). |
|
|
292
|
+
|
|
293
|
+
Both majors are covered, and the rules are gated by format so a 2.x document never picks up a 3.x rule. The 3.0-specific ones carry an `asyncapi-3-` prefix, matching Spectral's names so a `.spectral.yml` that re-severities individual rules keeps working.
|
|
294
|
+
|
|
295
|
+
The structural rules validate against the **official [`asyncapi/spec-json-schemas`](https://github.com/asyncapi/spec-json-schemas) meta-schemas, vendored as raw `.json`** ([`schemas/`](./src/rules/asyncapi/schemas/)). These are draft-07 documents that reference their own subschemas by absolute `$id` URI rather than by JSON Pointer — nothing is fetched, because every target is declared inside the same document. Each file is byte-for-byte upstream apart from **three regular expressions**, one of which is genuinely exponential on a failing input; each is replaced by a provably equivalent pattern with a single unambiguous quantifier, and the test suite asserts the equivalence over a generated corpus. See the [schemas README](./src/rules/asyncapi/schemas/README.md).
|
|
296
|
+
|
|
297
|
+
Structural validation runs **once per document, against the document as written** (`resolved: false`), matching the `oas*-schema` rules above. That is a deliberate choice rather than an oversight: validating the dereferenced tree instead re-checks every `components` entry once per `$ref` that reaches it, so a single mistake in a reusable message reported three times in a document that used it twice. The trade-off is what a `$ref` hides: content pulled in from another file is not structurally checked, and neither is a same-file reference that points at the wrong kind of object (a `messages` entry aimed at `components.schemas`, say) — the reference itself is well-formed, and only the dereferenced tree would show the mismatch. That is the same gap the OpenAPI preset has. Everything written in the document itself, `components` included, is validated exactly once.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
254
301
|
## Benchmarks
|
|
255
302
|
|
|
256
|
-
The `bench/` suite pits `@amritk/lint` head-to-head against **[Spectral](https://github.com/stoplightio/spectral)** — the OpenAPI linter this package is modelled on (hence the `spectral:oas` alias) — over the real-world specs the test suite lints: Swagger's petstore, the DigitalOcean API, and the OpenAI API (~17 KB to ~2.8 MB, spanning a small config and a genuinely large document). Both do the same job: **parse → dereference internal `$ref`s → run their recommended OpenAPI ruleset** (mjst dereferences in memory with [`@amritk/resolve-refs`](../resolve-refs), exactly as the CLI does; Spectral uses its own default resolver). Representative numbers (Bun 1.
|
|
303
|
+
The `bench/` suite pits `@amritk/lint` head-to-head against **[Spectral](https://github.com/stoplightio/spectral)** — the OpenAPI linter this package is modelled on (hence the `spectral:oas` alias) — over the real-world specs the test suite lints: Swagger's petstore, the DigitalOcean API, and the OpenAI API (~17 KB to ~2.8 MB, spanning a small config and a genuinely large document). Both do the same job: **parse → dereference internal `$ref`s → run their recommended OpenAPI ruleset** (mjst dereferences in memory with [`@amritk/resolve-refs`](../resolve-refs), exactly as the CLI does; Spectral uses its own default resolver). Representative numbers (Bun 1.4, Linux x64 — your hardware will differ, run `bun run bench` yourself):
|
|
257
304
|
|
|
258
305
|
| document | size | mjst | Spectral | speedup | findings (mjst / Spectral) |
|
|
259
306
|
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
260
|
-
| petstore (Swagger) | 17 KB | ~
|
|
261
|
-
| digitalocean | 105 KB | ~
|
|
262
|
-
| openai | 2.8 MB | ~
|
|
307
|
+
| petstore (Swagger) | 17 KB | ~5 ms | ~95 ms | **~19×** | 2 / 2 |
|
|
308
|
+
| digitalocean | 105 KB | ~27 ms | ~375 ms | **~14×** | 2411 / 4319 |
|
|
309
|
+
| openai | 2.8 MB | ~0.73 s | ~7.4 s | **~10×** | 1278 / 474 |
|
|
263
310
|
|
|
264
|
-
|
|
311
|
+
An earlier revision of this table reported the OpenAI row as mjst-only, because
|
|
312
|
+
Spectral's JSONPath engine (`nimma`) threw on that spec under Bun. It no longer
|
|
313
|
+
does, so the row is a real comparison now. The bench still guards the Spectral
|
|
314
|
+
side and reports `errored` rather than aborting the run, since that failure was
|
|
315
|
+
runtime-specific and may come back.
|
|
265
316
|
|
|
266
317
|
Each `lint` figure is the mean wall time of one whole pass — **every rule, not a subset** — dominated by real work: JSONPath matching, the rule functions, and the dereference pass. A fresh document is parsed on every iteration on both sides, matching how the tools are actually called. The finding counts differ because the two rulesets are not byte-identical (different rule implementations and `$ref` resolution), so this is a **throughput** comparison rather than a correctness parity check — but on petstore both land on the same two findings.
|
|
267
318
|
|
|
268
|
-
**Assembling the ruleset** is timed separately, because a process pays it once and then lints many documents: `createOpenApiRuleset` (compiling every rule's JSONPath and wiring up functions and format detectors) measures **~0.07 ms**, versus **~0.
|
|
319
|
+
**Assembling the ruleset** is timed separately, because a process pays it once and then lints many documents: `createOpenApiRuleset` (compiling every rule's JSONPath and wiring up functions and format detectors) measures **~0.07 ms**, versus **~0.28 ms** for `new Spectral()` + `setRuleset(oas)`. The benchmark warms up before timing and reports the mean over a fixed time budget; micro-benchmark figures vary by machine and runtime.
|
|
269
320
|
|
|
270
321
|
---
|
|
271
322
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const MAX_FILTER_DEPTH = 100;
|
|
1
2
|
const CONTEXT_TOKENS = {
|
|
2
3
|
"@": "value",
|
|
3
4
|
"@property": "property",
|
|
@@ -161,15 +162,20 @@ const ALLOWED_METHODS = /* @__PURE__ */ new Set([
|
|
|
161
162
|
"trim"
|
|
162
163
|
]);
|
|
163
164
|
const parseFilterExpression = (source) => {
|
|
165
|
+
const quoted = source.length > 120 ? `${source.slice(0, 120)}\u2026` : source;
|
|
166
|
+
const describe = (error) => {
|
|
167
|
+
const syntax = error;
|
|
168
|
+
return { error: `${syntax.message} at offset ${syntax.offset} in filter "${quoted}"` };
|
|
169
|
+
};
|
|
164
170
|
let tokens;
|
|
165
171
|
try {
|
|
166
172
|
tokens = tokenize(source);
|
|
167
173
|
} catch (error) {
|
|
168
|
-
|
|
169
|
-
return { error: `${syntax.message} at offset ${syntax.offset} in filter "${source}"` };
|
|
174
|
+
return describe(error);
|
|
170
175
|
}
|
|
171
176
|
let position = 0;
|
|
172
177
|
let usesPath = false;
|
|
178
|
+
let depth = 0;
|
|
173
179
|
const peek = () => tokens[position];
|
|
174
180
|
const offsetOf = (token) => token?.start ?? source.length;
|
|
175
181
|
const eatPunct = (text) => {
|
|
@@ -255,6 +261,14 @@ const parseFilterExpression = (source) => {
|
|
|
255
261
|
return fail(`Unexpected token "${token.text}"`, token.start);
|
|
256
262
|
};
|
|
257
263
|
const parseUnary = () => {
|
|
264
|
+
if (++depth > MAX_FILTER_DEPTH) {
|
|
265
|
+
fail(`Filter expression nests deeper than ${MAX_FILTER_DEPTH} levels`, offsetOf(peek()));
|
|
266
|
+
}
|
|
267
|
+
const node = parseUnaryInner();
|
|
268
|
+
depth--;
|
|
269
|
+
return node;
|
|
270
|
+
};
|
|
271
|
+
const parseUnaryInner = () => {
|
|
258
272
|
const token = peek();
|
|
259
273
|
if (token?.kind === "punct" && (token.text === "!" || token.text === "-")) {
|
|
260
274
|
position++;
|
|
@@ -311,8 +325,7 @@ const parseFilterExpression = (source) => {
|
|
|
311
325
|
fail(`Unexpected token "${"text" in trailing ? trailing.text : ""}"`, trailing.start);
|
|
312
326
|
return { node, usesPath };
|
|
313
327
|
} catch (error) {
|
|
314
|
-
|
|
315
|
-
return { error: `${syntax.message} at offset ${syntax.offset} in filter "${source}"` };
|
|
328
|
+
return describe(error);
|
|
316
329
|
}
|
|
317
330
|
};
|
|
318
331
|
export {
|
package/dist/core/filter.js
CHANGED
|
@@ -19,7 +19,7 @@ const readMember = (object, name) => {
|
|
|
19
19
|
return Number.isInteger(index) ? object[index] : void 0;
|
|
20
20
|
}
|
|
21
21
|
if (isPlainObject(object))
|
|
22
|
-
return Object.
|
|
22
|
+
return Object.prototype.propertyIsEnumerable.call(object, name) ? object[name] : void 0;
|
|
23
23
|
return void 0;
|
|
24
24
|
};
|
|
25
25
|
const toStringArgument = (value) => {
|
|
@@ -129,63 +129,110 @@ const compareRelational = (operator, left, right) => {
|
|
|
129
129
|
return a >= b;
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
|
-
const
|
|
132
|
+
const compileNode = (node) => {
|
|
133
133
|
switch (node.kind) {
|
|
134
134
|
case "literal":
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
case "regex": {
|
|
136
|
+
const constant = node.value;
|
|
137
|
+
return () => constant;
|
|
138
|
+
}
|
|
139
|
+
case "context": {
|
|
140
|
+
switch (node.ref) {
|
|
141
|
+
case "value":
|
|
142
|
+
return (value) => value;
|
|
143
|
+
case "property":
|
|
144
|
+
return (_value, property) => property;
|
|
145
|
+
case "parent":
|
|
146
|
+
return (_value, _property, parent) => parent;
|
|
147
|
+
case "parentProperty":
|
|
148
|
+
return (_value, _property, _parent, parentProperty) => parentProperty;
|
|
149
|
+
case "path":
|
|
150
|
+
return (_value, _property, _parent, _parentProperty, path) => path;
|
|
151
|
+
default:
|
|
152
|
+
return (_value, _property, _parent, _parentProperty, _path, root) => root;
|
|
146
153
|
}
|
|
147
|
-
return readMember(evaluate(node.object, context), index);
|
|
148
154
|
}
|
|
149
|
-
case "
|
|
150
|
-
|
|
155
|
+
case "member": {
|
|
156
|
+
const object = compileNode(node.object);
|
|
157
|
+
const name = node.name;
|
|
158
|
+
return (value, property, parent, parentProperty, path, root) => readMember(object(value, property, parent, parentProperty, path, root), name);
|
|
159
|
+
}
|
|
160
|
+
case "computed": {
|
|
161
|
+
const object = compileNode(node.object);
|
|
162
|
+
const index = compileNode(node.index);
|
|
163
|
+
return (value, property, parent, parentProperty, path, root) => {
|
|
164
|
+
const key = index(value, property, parent, parentProperty, path, root);
|
|
165
|
+
if (typeof key !== "string" && typeof key !== "number") {
|
|
166
|
+
throw filterRuntimeError("A computed member must be a string or a number");
|
|
167
|
+
}
|
|
168
|
+
return readMember(object(value, property, parent, parentProperty, path, root), key);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case "call": {
|
|
172
|
+
const object = compileNode(node.object);
|
|
173
|
+
const name = node.name;
|
|
174
|
+
const args = node.args.map(compileNode);
|
|
175
|
+
const arity = args.length;
|
|
176
|
+
return (value, property, parent, parentProperty, path, root) => {
|
|
177
|
+
const values = new Array(arity);
|
|
178
|
+
for (let i = 0; i < arity; i++) {
|
|
179
|
+
values[i] = args[i](value, property, parent, parentProperty, path, root);
|
|
180
|
+
}
|
|
181
|
+
return callMethod(object(value, property, parent, parentProperty, path, root), name, values);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
151
184
|
case "unary": {
|
|
152
|
-
|
|
153
|
-
|
|
185
|
+
const operand = compileNode(node.operand);
|
|
186
|
+
if (node.operator === "!") {
|
|
187
|
+
return (value, property, parent, parentProperty, path, root) => !operand(value, property, parent, parentProperty, path, root);
|
|
188
|
+
}
|
|
154
189
|
if (node.operator === "void") {
|
|
155
|
-
|
|
156
|
-
|
|
190
|
+
return (value, property, parent, parentProperty, path, root) => {
|
|
191
|
+
operand(value, property, parent, parentProperty, path, root);
|
|
192
|
+
return void 0;
|
|
193
|
+
};
|
|
157
194
|
}
|
|
158
|
-
return -Number(
|
|
195
|
+
return (value, property, parent, parentProperty, path, root) => -Number(operand(value, property, parent, parentProperty, path, root));
|
|
159
196
|
}
|
|
160
197
|
case "logical": {
|
|
161
|
-
const left =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
198
|
+
const left = compileNode(node.left);
|
|
199
|
+
const right = compileNode(node.right);
|
|
200
|
+
if (node.operator === "&&") {
|
|
201
|
+
return (value, property, parent, parentProperty, path, root) => {
|
|
202
|
+
const outcome = left(value, property, parent, parentProperty, path, root);
|
|
203
|
+
return outcome ? right(value, property, parent, parentProperty, path, root) : outcome;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return (value, property, parent, parentProperty, path, root) => {
|
|
207
|
+
const outcome = left(value, property, parent, parentProperty, path, root);
|
|
208
|
+
return outcome ? outcome : right(value, property, parent, parentProperty, path, root);
|
|
209
|
+
};
|
|
165
210
|
}
|
|
166
211
|
case "comparison": {
|
|
167
|
-
const left =
|
|
168
|
-
const right =
|
|
212
|
+
const left = compileNode(node.left);
|
|
213
|
+
const right = compileNode(node.right);
|
|
169
214
|
switch (node.operator) {
|
|
170
215
|
case "===":
|
|
171
|
-
return left === right;
|
|
216
|
+
return (value, property, parent, parentProperty, path, root) => left(value, property, parent, parentProperty, path, root) === right(value, property, parent, parentProperty, path, root);
|
|
172
217
|
case "!==":
|
|
173
|
-
return left !== right;
|
|
218
|
+
return (value, property, parent, parentProperty, path, root) => left(value, property, parent, parentProperty, path, root) !== right(value, property, parent, parentProperty, path, root);
|
|
174
219
|
case "==":
|
|
175
|
-
return looseEquals(left, right);
|
|
220
|
+
return (value, property, parent, parentProperty, path, root) => looseEquals(left(value, property, parent, parentProperty, path, root), right(value, property, parent, parentProperty, path, root));
|
|
176
221
|
case "!=":
|
|
177
|
-
return !looseEquals(left, right);
|
|
178
|
-
default:
|
|
179
|
-
|
|
222
|
+
return (value, property, parent, parentProperty, path, root) => !looseEquals(left(value, property, parent, parentProperty, path, root), right(value, property, parent, parentProperty, path, root));
|
|
223
|
+
default: {
|
|
224
|
+
const operator = node.operator;
|
|
225
|
+
return (value, property, parent, parentProperty, path, root) => compareRelational(operator, left(value, property, parent, parentProperty, path, root), right(value, property, parent, parentProperty, path, root));
|
|
226
|
+
}
|
|
180
227
|
}
|
|
181
228
|
}
|
|
182
229
|
}
|
|
183
230
|
};
|
|
184
231
|
const toFilterFn = (parsed) => {
|
|
185
|
-
const
|
|
232
|
+
const test = compileNode(parsed.node);
|
|
186
233
|
return (value, property, parent, root, path, parentProperty) => {
|
|
187
234
|
try {
|
|
188
|
-
return Boolean(
|
|
235
|
+
return Boolean(test(value, property, parent, parentProperty, path, root));
|
|
189
236
|
} catch {
|
|
190
237
|
return false;
|
|
191
238
|
}
|
package/dist/core/glob.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createBoundedCache } from "./bounded-cache.js";
|
|
1
2
|
const REGEXP_SPECIAL = /[\\^$.*+?()[\]{}|/]/;
|
|
2
3
|
const splitTopLevel = (body) => {
|
|
3
4
|
const parts = [];
|
|
@@ -18,46 +19,40 @@ const splitTopLevel = (body) => {
|
|
|
18
19
|
parts.push(current);
|
|
19
20
|
return parts;
|
|
20
21
|
};
|
|
21
|
-
const
|
|
22
|
-
const open = glob.indexOf("{");
|
|
23
|
-
if (open === -1)
|
|
24
|
-
return [glob];
|
|
22
|
+
const findBraceEnd = (glob, open) => {
|
|
25
23
|
let depth = 0;
|
|
26
|
-
let close = -1;
|
|
27
24
|
for (let i = open; i < glob.length; i++) {
|
|
28
25
|
if (glob[i] === "{")
|
|
29
26
|
depth++;
|
|
30
27
|
else if (glob[i] === "}") {
|
|
31
28
|
depth--;
|
|
32
|
-
if (depth === 0)
|
|
33
|
-
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (close === -1)
|
|
39
|
-
return [glob];
|
|
40
|
-
const prefix = glob.slice(0, open);
|
|
41
|
-
const body = glob.slice(open + 1, close);
|
|
42
|
-
const suffix = glob.slice(close + 1);
|
|
43
|
-
const options = splitTopLevel(body);
|
|
44
|
-
if (options.length === 1) {
|
|
45
|
-
return expandBraces(suffix).map((rest) => `${prefix}{${body}}${rest}`);
|
|
46
|
-
}
|
|
47
|
-
const results = [];
|
|
48
|
-
for (const option of options) {
|
|
49
|
-
for (const expandedOption of expandBraces(option)) {
|
|
50
|
-
for (const expandedSuffix of expandBraces(suffix)) {
|
|
51
|
-
results.push(prefix + expandedOption + expandedSuffix);
|
|
52
|
-
}
|
|
29
|
+
if (depth === 0)
|
|
30
|
+
return i;
|
|
53
31
|
}
|
|
54
32
|
}
|
|
55
|
-
return
|
|
33
|
+
return -1;
|
|
56
34
|
};
|
|
57
|
-
const
|
|
35
|
+
const globSource = (glob, braces) => {
|
|
58
36
|
let source = "";
|
|
59
37
|
for (let i = 0; i < glob.length; i++) {
|
|
60
38
|
const char = glob[i];
|
|
39
|
+
if (char === "{" && braces) {
|
|
40
|
+
const close = findBraceEnd(glob, i);
|
|
41
|
+
if (close === -1)
|
|
42
|
+
return source + globSource(glob.slice(i), false);
|
|
43
|
+
const body = glob.slice(i + 1, close);
|
|
44
|
+
const options = splitTopLevel(body);
|
|
45
|
+
if (options.length === 1) {
|
|
46
|
+
source += `\\{${globSource(body, false)}\\}`;
|
|
47
|
+
i = close;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const followedBySlash = glob[close + 1] === "/";
|
|
51
|
+
const compile = (option) => globSource(followedBySlash ? `${option}/` : option, true);
|
|
52
|
+
source += `(?:${options.map(compile).join("|")})`;
|
|
53
|
+
i = followedBySlash ? close + 1 : close;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
61
56
|
if (char === "*") {
|
|
62
57
|
if (glob[i + 1] === "*") {
|
|
63
58
|
i++;
|
|
@@ -80,13 +75,12 @@ const globInnerSource = (glob) => {
|
|
|
80
75
|
}
|
|
81
76
|
return source;
|
|
82
77
|
};
|
|
83
|
-
const regexCache =
|
|
78
|
+
const regexCache = createBoundedCache(500);
|
|
84
79
|
const globToRegExp = (glob) => {
|
|
85
80
|
const cached = regexCache.get(glob);
|
|
86
81
|
if (cached)
|
|
87
82
|
return cached;
|
|
88
|
-
const
|
|
89
|
-
const regex = new RegExp(`^(?:${alternatives.join("|")})$`);
|
|
83
|
+
const regex = new RegExp(`^(?:${globSource(glob, true)})$`);
|
|
90
84
|
regexCache.set(glob, regex);
|
|
91
85
|
return regex;
|
|
92
86
|
};
|
package/dist/core/jsonpath.d.ts
CHANGED
|
@@ -5,9 +5,17 @@ export type IQueryMatch = {
|
|
|
5
5
|
value: unknown;
|
|
6
6
|
path: JsonPath;
|
|
7
7
|
};
|
|
8
|
-
type Selector =
|
|
8
|
+
type Selector =
|
|
9
|
+
/**
|
|
10
|
+
* `.name` / `['name']`. `index` is the same name read as an array index, or
|
|
11
|
+
* `undefined` when it is not all digits — precomputed here because the check
|
|
12
|
+
* used to run as a regex against the *same* name on every array node the
|
|
13
|
+
* descent reached.
|
|
14
|
+
*/
|
|
15
|
+
{
|
|
9
16
|
kind: 'child';
|
|
10
17
|
name: string;
|
|
18
|
+
index: number | undefined;
|
|
11
19
|
} | {
|
|
12
20
|
kind: 'index';
|
|
13
21
|
index: number;
|