@amritk/lint 0.0.0 → 0.2.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/README.md +34 -1
- package/dist/core/document.d.ts +19 -0
- package/dist/core/document.js +11 -0
- package/dist/core/formats.d.ts +9 -0
- package/dist/core/formats.js +14 -0
- package/dist/core/glob.d.ts +4 -0
- package/dist/core/glob.js +48 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +11 -0
- package/dist/core/jsonpath.d.ts +59 -0
- package/dist/core/jsonpath.js +415 -0
- package/dist/core/lint.d.ts +57 -0
- package/dist/core/lint.js +82 -0
- package/dist/core/plugin.d.ts +59 -0
- package/dist/core/plugin.js +25 -0
- package/dist/core/pointers.d.ts +35 -0
- package/dist/core/pointers.js +169 -0
- package/dist/core/ruleset.d.ts +52 -0
- package/dist/core/ruleset.js +170 -0
- package/dist/core/runner.d.ts +21 -0
- package/dist/core/runner.js +222 -0
- package/dist/core/types.d.ts +170 -0
- package/dist/core/types.js +1 -0
- package/dist/core/validate-ruleset.d.ts +14 -0
- package/dist/core/validate-ruleset.js +105 -0
- package/dist/fix/apply.d.ts +21 -0
- package/dist/fix/apply.js +51 -0
- package/dist/fix/index.d.ts +3 -0
- package/dist/fix/index.js +2 -0
- package/dist/fix/plugin.d.ts +18 -0
- package/dist/fix/plugin.js +21 -0
- package/dist/fix/types.d.ts +39 -0
- package/dist/fix/types.js +0 -0
- package/dist/functions/alphabetical.d.ts +8 -0
- package/dist/functions/alphabetical.js +28 -0
- package/dist/functions/casing.d.ts +14 -0
- package/dist/functions/casing.js +30 -0
- package/dist/functions/defined.d.ts +3 -0
- package/dist/functions/defined.js +6 -0
- package/dist/functions/enumeration.d.ts +5 -0
- package/dist/functions/enumeration.js +10 -0
- package/dist/functions/falsy.d.ts +3 -0
- package/dist/functions/falsy.js +6 -0
- package/dist/functions/index.d.ts +16 -0
- package/dist/functions/index.js +42 -0
- package/dist/functions/length.d.ts +6 -0
- package/dist/functions/length.js +27 -0
- package/dist/functions/pattern.d.ts +6 -0
- package/dist/functions/pattern.js +20 -0
- package/dist/functions/schema.d.ts +8 -0
- package/dist/functions/schema.js +36 -0
- package/dist/functions/truthy.d.ts +3 -0
- package/dist/functions/truthy.js +6 -0
- package/dist/functions/typed-enum.d.ts +3 -0
- package/dist/functions/typed-enum.js +34 -0
- package/dist/functions/undefined.d.ts +6 -0
- package/dist/functions/undefined.js +9 -0
- package/dist/functions/unreferenced-reusable-object.d.ts +8 -0
- package/dist/functions/unreferenced-reusable-object.js +37 -0
- package/dist/functions/xor.d.ts +7 -0
- package/dist/functions/xor.js +11 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +168 -0
- package/dist/parsers/edit-model.d.ts +69 -0
- package/dist/parsers/edit-model.js +326 -0
- package/dist/parsers/index.d.ts +18 -0
- package/dist/parsers/index.js +21 -0
- package/dist/parsers/json.d.ts +3 -0
- package/dist/parsers/json.js +38 -0
- package/dist/parsers/lines.d.ts +13 -0
- package/dist/parsers/lines.js +28 -0
- package/dist/parsers/types.d.ts +50 -0
- package/dist/parsers/types.js +8 -0
- package/dist/parsers/yaml.d.ts +6 -0
- package/dist/parsers/yaml.js +65 -0
- package/dist/rules/openapi/fixers.d.ts +7 -0
- package/dist/rules/openapi/fixers.js +189 -0
- package/dist/rules/openapi/formats.d.ts +13 -0
- package/dist/rules/openapi/formats.js +23 -0
- package/dist/rules/openapi/functions/example-validation.d.ts +5 -0
- package/dist/rules/openapi/functions/example-validation.js +97 -0
- package/dist/rules/openapi/functions/helpers.d.ts +3 -0
- package/dist/rules/openapi/functions/helpers.js +5 -0
- package/dist/rules/openapi/functions/index.d.ts +24 -0
- package/dist/rules/openapi/functions/index.js +67 -0
- package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
- package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
- package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
- package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-example-value.js +31 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -0
- package/dist/rules/openapi/functions/oas-op-params.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-params.js +21 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
- package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-success-response.js +11 -0
- package/dist/rules/openapi/functions/oas-path-param.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-path-param.js +45 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
- package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
- package/dist/rules/openapi/functions/oas-schema.js +15 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-server-variables.js +23 -0
- package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
- package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
- package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-unused-component.js +52 -0
- package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
- package/dist/rules/openapi/functions/ref-siblings.js +13 -0
- package/dist/rules/openapi/index.d.ts +25 -0
- package/dist/rules/openapi/index.js +127 -0
- package/dist/rules/openapi/oas.d.ts +3 -0
- package/dist/rules/openapi/oas.js +492 -0
- package/dist/rules/openapi/schemas/index.d.ts +8 -0
- package/dist/rules/openapi/schemas/index.js +36 -0
- package/dist/rules/openapi/schemas/oas20.json +1592 -0
- package/dist/rules/openapi/schemas/oas30.json +1651 -0
- package/dist/rules/openapi/schemas/oas31.json +1412 -0
- package/dist/rules/openapi/schemas/oas32.json +1684 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
`@amritk/lint` lints **any** JSON or YAML document against a ruleset you define. A rule matches nodes with a **JSONPath** (`given`) and runs a **function** (`then`) over each match — structural validation against a **JSON Schema**, style checks (`casing`, `pattern`, `alphabetical`, `length`, …), or your own custom function. Every finding carries an exact `line:column` range, because the parser keeps source positions on every node.
|
|
21
21
|
|
|
22
|
-
It is **format-agnostic**: the 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
|
|
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
|
+
|
|
24
|
+
For OpenAPI specifically, the `@amritk/lint/rules/openapi` subpath ships a ready-made preset on top of that engine — see [OpenAPI ruleset](#openapi-ruleset) below.
|
|
23
25
|
|
|
24
26
|
The CLI lives in the [`mjst`](../cli) binary as `mjst lint`; this package is the programmatic library behind it.
|
|
25
27
|
|
|
@@ -181,6 +183,37 @@ The engine internals (`createDocument`, `lint`, `query`, `validateRuleset`, `par
|
|
|
181
183
|
|
|
182
184
|
---
|
|
183
185
|
|
|
186
|
+
## OpenAPI ruleset
|
|
187
|
+
|
|
188
|
+
The core package is format-agnostic, but OpenAPI is common enough to ship a ready-made preset. It lives at the **`@amritk/lint/rules/openapi`** subpath — a self-contained layer on top of the engine that adds **no dependencies** beyond what `@amritk/lint` already uses.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { lint } from '@amritk/lint'
|
|
192
|
+
import { createOpenApiRuleset } from '@amritk/lint/rules/openapi'
|
|
193
|
+
|
|
194
|
+
// Defaults to `extends: [oas]` (recommended rules only, like `spectral:oas`).
|
|
195
|
+
const ruleset = createOpenApiRuleset()
|
|
196
|
+
const findings = await lint(spec, { ruleset })
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`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.
|
|
200
|
+
|
|
201
|
+
| Export | What it does |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `createOpenApiRuleset(definition?, basePath?)` | Build a runnable OpenAPI `Ruleset` (functions + formats + `extends` resolution). |
|
|
204
|
+
| `resolveOpenApiRuleset(name, basePath?)` | Resolve an `extends` reference, including the `oas` / `loupe:oas` / `spectral:oas` names. |
|
|
205
|
+
| `oas` | The built-in OpenAPI ruleset definition. |
|
|
206
|
+
| `oasFunctions` / `allFunctions` | The OpenAPI-specific functions; `allFunctions` = built-ins + OpenAPI. |
|
|
207
|
+
| `oasFormats` | OpenAPI version detectors (`oas2`, `oas3`, `oas3.0`, `oas3.1`, `oas3.2`). |
|
|
208
|
+
| `oasFixers` | Auto-fixers for the mechanically-repairable OpenAPI rules (pass to `fixDocument`). |
|
|
209
|
+
| `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). |
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
`$ref` resolution stays the caller's job: the preset doesn't pull in a resolver, so for rules that need the dereferenced document (`resolved: true`) pass a `resolve` function to the core `lintWithResult` (for example wrapping [`@amritk/resolve-refs`](../resolve-refs)). The `mjst lint` CLI already wires one up.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
184
217
|
## License
|
|
185
218
|
|
|
186
219
|
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type IDiagnostic, type ILocation, type IParserOptions, type JsonPath, type ParserFormat } from '../parsers/index.js';
|
|
2
|
+
/** Options for {@link createDocument}: the parser options plus a display `source` and an explicit `format`. */
|
|
3
|
+
export type IDocumentOptions = IParserOptions & {
|
|
4
|
+
source?: string;
|
|
5
|
+
format?: ParserFormat;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A source string paired with its parsed data and the source map that resolves
|
|
9
|
+
* JSON paths back to line:column ranges. Parsing happens up front in
|
|
10
|
+
* {@link createDocument}, so every field here is ready to read.
|
|
11
|
+
*/
|
|
12
|
+
export type Document<T = unknown> = {
|
|
13
|
+
readonly source?: string | undefined;
|
|
14
|
+
readonly data: T;
|
|
15
|
+
readonly diagnostics: IDiagnostic[];
|
|
16
|
+
getLocationForJsonPath(path: JsonPath, closest?: boolean): ILocation | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** Parses `input` into a {@link Document} with a source map for position lookups. */
|
|
19
|
+
export declare const createDocument: <T = unknown>(input: string, options?: IDocumentOptions) => Document<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseWithPointers, } from '../parsers/index.js';
|
|
2
|
+
/** Parses `input` into a {@link Document} with a source map for position lookups. */
|
|
3
|
+
export const createDocument = (input, options = {}) => {
|
|
4
|
+
const parsed = parseWithPointers(input, options);
|
|
5
|
+
return {
|
|
6
|
+
source: options.source,
|
|
7
|
+
data: parsed.data,
|
|
8
|
+
diagnostics: parsed.diagnostics,
|
|
9
|
+
getLocationForJsonPath: (path, closest = false) => parsed.getLocationForJsonPath(path, closest),
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A predicate that reports whether a parsed document matches a given format. */
|
|
2
|
+
export type Format = (document: unknown) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the set of registered format names that match the document. The
|
|
5
|
+
* `formats` registry is supplied by the caller (a preset provides its own
|
|
6
|
+
* format detectors); the engine itself is format-agnostic, so the
|
|
7
|
+
* default registry is empty and rules with no `formats` gate run regardless.
|
|
8
|
+
*/
|
|
9
|
+
export declare const detectFormats: (document: unknown, formats?: Record<string, Format>) => Set<string>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the set of registered format names that match the document. The
|
|
3
|
+
* `formats` registry is supplied by the caller (a preset provides its own
|
|
4
|
+
* format detectors); the engine itself is format-agnostic, so the
|
|
5
|
+
* default registry is empty and rules with no `formats` gate run regardless.
|
|
6
|
+
*/
|
|
7
|
+
export const detectFormats = (document, formats = {}) => {
|
|
8
|
+
const matched = new Set();
|
|
9
|
+
for (const [name, detector] of Object.entries(formats)) {
|
|
10
|
+
if (detector(document))
|
|
11
|
+
matched.add(name);
|
|
12
|
+
}
|
|
13
|
+
return matched;
|
|
14
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Compiles a glob pattern (`**`, `*`, `?`) into an anchored RegExp. */
|
|
2
|
+
export declare const globToRegExp: (glob: string) => RegExp;
|
|
3
|
+
/** Returns true if `path` matches any of the glob patterns. */
|
|
4
|
+
export declare const matchesGlob: (path: string, patterns: string[]) => boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal glob → RegExp for matching override `files` patterns against document
|
|
3
|
+
* paths. Supports `**` (any path segments), `*` (within a segment), and `?`.
|
|
4
|
+
*/
|
|
5
|
+
const REGEXP_SPECIAL = /[\\^$.*+?()[\]{}|/]/;
|
|
6
|
+
/** Compiles a glob pattern (`**`, `*`, `?`) into an anchored RegExp. */
|
|
7
|
+
export const globToRegExp = (glob) => {
|
|
8
|
+
let source = '';
|
|
9
|
+
for (let i = 0; i < glob.length; i++) {
|
|
10
|
+
const char = glob[i];
|
|
11
|
+
if (char === '*') {
|
|
12
|
+
if (glob[i + 1] === '*') {
|
|
13
|
+
i++;
|
|
14
|
+
if (glob[i + 1] === '/') {
|
|
15
|
+
i++;
|
|
16
|
+
source += '(?:.*/)?';
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
source += '.*';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
source += '[^/]*';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (char === '?') {
|
|
27
|
+
source += '[^/]';
|
|
28
|
+
}
|
|
29
|
+
else if (char && REGEXP_SPECIAL.test(char)) {
|
|
30
|
+
source += `\\${char}`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
source += char;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new RegExp(`^${source}$`);
|
|
37
|
+
};
|
|
38
|
+
/** Returns true if `path` matches any of the glob patterns. */
|
|
39
|
+
export const matchesGlob = (path, patterns) => {
|
|
40
|
+
const basename = path.split('/').pop() ?? path;
|
|
41
|
+
return patterns.some((pattern) => {
|
|
42
|
+
const regex = globToRegExp(pattern);
|
|
43
|
+
if (regex.test(path))
|
|
44
|
+
return true;
|
|
45
|
+
// A pattern without a slash also matches the basename.
|
|
46
|
+
return !pattern.includes('/') && regex.test(basename);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createDocument, type Document, type IDocumentOptions } from './document.js';
|
|
2
|
+
export { detectFormats, type Format } from './formats.js';
|
|
3
|
+
export { globToRegExp, matchesGlob } from './glob.js';
|
|
4
|
+
export { type CompiledPath, compileQuery, type IQueryMatch, query, queryCompiled, queryMany } from './jsonpath.js';
|
|
5
|
+
export { type LintOptions, type LintResolver, type LintResult, lint, lintWithResult } from './lint.js';
|
|
6
|
+
export { type LintPlugin, type LintPluginContext, type LintPluginResult, type PluginRunResult, runPlugins, } from './plugin.js';
|
|
7
|
+
export { pointerToPath, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, } from './pointers.js';
|
|
8
|
+
export { type AliasDefinition, createRuleset, type ExtendModifier, type ExtendResolver, type ResolvedExtend, type Ruleset, type RulesetOptions, } from './ruleset.js';
|
|
9
|
+
export { createLinter, type IRunOptions, type Linter } from './runner.js';
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
export { type IRulesetProblem, validateRuleset } from './validate-ruleset.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createDocument } from './document.js';
|
|
2
|
+
export { detectFormats } from './formats.js';
|
|
3
|
+
export { globToRegExp, matchesGlob } from './glob.js';
|
|
4
|
+
export { compileQuery, query, queryCompiled, queryMany } from './jsonpath.js';
|
|
5
|
+
export { lint, lintWithResult } from './lint.js';
|
|
6
|
+
export { runPlugins, } from './plugin.js';
|
|
7
|
+
export { pointerToPath, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, } from './pointers.js';
|
|
8
|
+
export { createRuleset, } from './ruleset.js';
|
|
9
|
+
export { createLinter } from './runner.js';
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
export { validateRuleset } from './validate-ruleset.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { JsonPath } from './types.js';
|
|
2
|
+
/** A single JSONPath match: the matched value and its concrete path from the root. */
|
|
3
|
+
export type IQueryMatch = {
|
|
4
|
+
value: unknown;
|
|
5
|
+
path: JsonPath;
|
|
6
|
+
};
|
|
7
|
+
type FilterFn = (value: unknown, property: string | number | undefined, parent: unknown, root: unknown, path: JsonPath, parentProperty: string | number | undefined) => boolean;
|
|
8
|
+
type Selector = {
|
|
9
|
+
kind: 'child';
|
|
10
|
+
name: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'index';
|
|
13
|
+
index: number;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'wildcard';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'union';
|
|
18
|
+
names: (string | number)[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'filter';
|
|
21
|
+
test: FilterFn;
|
|
22
|
+
source: string;
|
|
23
|
+
usesPath: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'parent';
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'keys';
|
|
28
|
+
};
|
|
29
|
+
/** One compiled segment of a path: a selector and whether it follows a `..` descent. */
|
|
30
|
+
export type Step = {
|
|
31
|
+
/** Whether this step is reached via `..` (descendant-or-self). */
|
|
32
|
+
recursive: boolean;
|
|
33
|
+
selector: Selector;
|
|
34
|
+
};
|
|
35
|
+
/** A JSONPath expression compiled into flat steps; the runner's query planner groups on these. */
|
|
36
|
+
export type CompiledPath = {
|
|
37
|
+
readonly expression: string;
|
|
38
|
+
readonly steps: Step[];
|
|
39
|
+
/** True when the path contains at least one `..` step. */
|
|
40
|
+
readonly hasDescent: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** Compiles a JSONPath `expression` into a {@link CompiledPath}, cached by string so repeats are free. */
|
|
43
|
+
export declare const compileQuery: (expression: string) => CompiledPath;
|
|
44
|
+
/** Evaluates a pre-compiled path against `data`. */
|
|
45
|
+
export declare const queryCompiled: (data: unknown, compiled: CompiledPath) => IQueryMatch[];
|
|
46
|
+
/**
|
|
47
|
+
* Evaluates many pre-compiled paths against `data`, sharing a *single* recursive
|
|
48
|
+
* descent of the tree across every `$..`-rooted path. The ruleset has ~16
|
|
49
|
+
* descent `given`s; walking the (post-deref, ~60k-node) tree once and testing
|
|
50
|
+
* each path's first selector at every node — instead of one full traversal per
|
|
51
|
+
* path — is the dominant rule-run speedup on large specs. Non-recursive paths
|
|
52
|
+
* are evaluated directly (cheap, no descent).
|
|
53
|
+
*
|
|
54
|
+
* Returns one match array per input path, index-aligned with `compiled`.
|
|
55
|
+
*/
|
|
56
|
+
export declare const queryMany: (data: unknown, compiled: CompiledPath[]) => IQueryMatch[][];
|
|
57
|
+
/** Runs a JSONPath expression and returns each match with its concrete path. */
|
|
58
|
+
export declare const query: (data: unknown, expression: string) => IQueryMatch[];
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
|
+
// jsonpath-plus emits numeric array indices as strings and Linter historically
|
|
3
|
+
// normalized *any* all-digit segment (including object keys like "200") to a
|
|
4
|
+
// number. Replicate that exactly so source-map lookups are unchanged.
|
|
5
|
+
const normalizeSegment = (segment) => {
|
|
6
|
+
if (typeof segment === 'number')
|
|
7
|
+
return segment;
|
|
8
|
+
if (segment.length > 0 && /^\d+$/.test(segment))
|
|
9
|
+
return Number(segment);
|
|
10
|
+
return segment;
|
|
11
|
+
};
|
|
12
|
+
const normalizePath = (path) => path.map(normalizeSegment);
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Compilation
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const compileCache = new Map();
|
|
17
|
+
const filterCache = new Map();
|
|
18
|
+
const compileFilter = (source) => {
|
|
19
|
+
const cached = filterCache.get(source);
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
// Map jsonpath-plus' `@`-context tokens onto real identifiers, longest first
|
|
23
|
+
// so `@parentProperty` is not eaten by `@parent`.
|
|
24
|
+
const body = source
|
|
25
|
+
.replace(/@parentProperty/g, '_pp')
|
|
26
|
+
.replace(/@parent/g, '_parent')
|
|
27
|
+
.replace(/@property/g, '_prop')
|
|
28
|
+
.replace(/@path/g, '_path')
|
|
29
|
+
.replace(/@root/g, '_root')
|
|
30
|
+
.replace(/@/g, '_v');
|
|
31
|
+
let fn;
|
|
32
|
+
try {
|
|
33
|
+
// `_pp` (`@parentProperty`) is supplied directly by the caller rather than
|
|
34
|
+
// derived from a materialized path, so most filters never force a path
|
|
35
|
+
// allocation. `_path` (`@path`) is only materialized for filters that use it.
|
|
36
|
+
const compiled = new Function('_v', '_prop', '_parent', '_root', '_path', '_pp', `try { return !!(${body}); } catch (_e) { return false; }`);
|
|
37
|
+
fn = compiled;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
fn = () => false;
|
|
41
|
+
}
|
|
42
|
+
filterCache.set(source, fn);
|
|
43
|
+
return fn;
|
|
44
|
+
};
|
|
45
|
+
/** Splits bracket content on top-level commas, respecting quotes. */
|
|
46
|
+
const splitUnion = (content) => {
|
|
47
|
+
const parts = [];
|
|
48
|
+
let depth = 0;
|
|
49
|
+
let quote = '';
|
|
50
|
+
let current = '';
|
|
51
|
+
for (let i = 0; i < content.length; i++) {
|
|
52
|
+
const ch = content[i];
|
|
53
|
+
if (quote) {
|
|
54
|
+
if (ch === quote)
|
|
55
|
+
quote = '';
|
|
56
|
+
current += ch;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '"' || ch === "'") {
|
|
60
|
+
quote = ch;
|
|
61
|
+
current += ch;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '[' || ch === '(')
|
|
65
|
+
depth++;
|
|
66
|
+
else if (ch === ']' || ch === ')')
|
|
67
|
+
depth--;
|
|
68
|
+
if (ch === ',' && depth === 0) {
|
|
69
|
+
parts.push(current);
|
|
70
|
+
current = '';
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
current += ch;
|
|
74
|
+
}
|
|
75
|
+
parts.push(current);
|
|
76
|
+
return parts;
|
|
77
|
+
};
|
|
78
|
+
const unquote = (token) => {
|
|
79
|
+
const t = token.trim();
|
|
80
|
+
if (t.length >= 2 && (t[0] === '"' || t[0] === "'") && t[t.length - 1] === t[0]) {
|
|
81
|
+
return t.slice(1, -1);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
const bracketSelector = (content) => {
|
|
86
|
+
const trimmed = content.trim();
|
|
87
|
+
if (trimmed === '*')
|
|
88
|
+
return { kind: 'wildcard' };
|
|
89
|
+
if (trimmed.startsWith('?')) {
|
|
90
|
+
// `?(expr)` — extract the inner expression between the first `(` and last `)`.
|
|
91
|
+
const open = trimmed.indexOf('(');
|
|
92
|
+
const close = trimmed.lastIndexOf(')');
|
|
93
|
+
const expr = open !== -1 && close > open ? trimmed.slice(open + 1, close) : trimmed.slice(1);
|
|
94
|
+
return { kind: 'filter', test: compileFilter(expr), source: expr, usesPath: expr.includes('@path') };
|
|
95
|
+
}
|
|
96
|
+
const parts = splitUnion(content);
|
|
97
|
+
const names = [];
|
|
98
|
+
for (const part of parts) {
|
|
99
|
+
const token = part.trim();
|
|
100
|
+
const literal = unquote(token);
|
|
101
|
+
if (literal !== null) {
|
|
102
|
+
names.push(literal);
|
|
103
|
+
}
|
|
104
|
+
else if (/^-?\d+$/.test(token)) {
|
|
105
|
+
names.push(Number(token));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
names.push(token);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (names.length === 1) {
|
|
112
|
+
const only = names[0];
|
|
113
|
+
return typeof only === 'number' ? { kind: 'index', index: only } : { kind: 'child', name: only };
|
|
114
|
+
}
|
|
115
|
+
return { kind: 'union', names };
|
|
116
|
+
};
|
|
117
|
+
/** Finds the index of the `]` that closes the `[` at `start`, respecting quotes/nesting. */
|
|
118
|
+
const findBracketEnd = (expression, start) => {
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let quote = '';
|
|
121
|
+
for (let i = start; i < expression.length; i++) {
|
|
122
|
+
const ch = expression[i];
|
|
123
|
+
if (quote) {
|
|
124
|
+
if (ch === quote)
|
|
125
|
+
quote = '';
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '"' || ch === "'")
|
|
129
|
+
quote = ch;
|
|
130
|
+
else if (ch === '[' || ch === '(')
|
|
131
|
+
depth++;
|
|
132
|
+
else if (ch === ')')
|
|
133
|
+
depth--;
|
|
134
|
+
else if (ch === ']') {
|
|
135
|
+
depth--;
|
|
136
|
+
if (depth === 0)
|
|
137
|
+
return i;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return -1;
|
|
141
|
+
};
|
|
142
|
+
const readName = (expression, start) => {
|
|
143
|
+
let i = start;
|
|
144
|
+
while (i < expression.length && !'.[]^~'.includes(expression[i]))
|
|
145
|
+
i++;
|
|
146
|
+
return { name: expression.slice(start, i), end: i };
|
|
147
|
+
};
|
|
148
|
+
/** Compiles a JSONPath `expression` into a {@link CompiledPath}, cached by string so repeats are free. */
|
|
149
|
+
export const compileQuery = (expression) => {
|
|
150
|
+
const cached = compileCache.get(expression);
|
|
151
|
+
if (cached)
|
|
152
|
+
return cached;
|
|
153
|
+
const steps = [];
|
|
154
|
+
let hasDescent = false;
|
|
155
|
+
let i = 0;
|
|
156
|
+
if (expression[0] === '$')
|
|
157
|
+
i = 1;
|
|
158
|
+
let recursive = false;
|
|
159
|
+
while (i < expression.length) {
|
|
160
|
+
const ch = expression[i];
|
|
161
|
+
if (ch === '.') {
|
|
162
|
+
if (expression[i + 1] === '.') {
|
|
163
|
+
recursive = true;
|
|
164
|
+
hasDescent = true;
|
|
165
|
+
i += 2;
|
|
166
|
+
// A bare `..` followed by `.`/end is unusual; loop handles the selector.
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
i++;
|
|
170
|
+
if (expression[i] === '*') {
|
|
171
|
+
steps.push({ recursive, selector: { kind: 'wildcard' } });
|
|
172
|
+
recursive = false;
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const { name, end } = readName(expression, i);
|
|
177
|
+
steps.push({ recursive, selector: { kind: 'child', name } });
|
|
178
|
+
recursive = false;
|
|
179
|
+
i = end;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (ch === '[') {
|
|
183
|
+
const end = findBracketEnd(expression, i);
|
|
184
|
+
if (end === -1)
|
|
185
|
+
break;
|
|
186
|
+
const content = expression.slice(i + 1, end);
|
|
187
|
+
steps.push({ recursive, selector: bracketSelector(content) });
|
|
188
|
+
recursive = false;
|
|
189
|
+
i = end + 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (ch === '^') {
|
|
193
|
+
steps.push({ recursive: false, selector: { kind: 'parent' } });
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (ch === '~') {
|
|
198
|
+
steps.push({ recursive: false, selector: { kind: 'keys' } });
|
|
199
|
+
i++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch === '*') {
|
|
203
|
+
steps.push({ recursive, selector: { kind: 'wildcard' } });
|
|
204
|
+
recursive = false;
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Bare name following `..` (e.g. `$..foo`) or other unexpected token.
|
|
209
|
+
if (recursive) {
|
|
210
|
+
const { name, end } = readName(expression, i);
|
|
211
|
+
if (end > i) {
|
|
212
|
+
steps.push({ recursive: true, selector: { kind: 'child', name } });
|
|
213
|
+
recursive = false;
|
|
214
|
+
i = end;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
const compiled = { expression, steps, hasDescent };
|
|
221
|
+
compileCache.set(expression, compiled);
|
|
222
|
+
return compiled;
|
|
223
|
+
};
|
|
224
|
+
const EMPTY_PATH = [];
|
|
225
|
+
/** Materializes the concrete (un-normalized) path from root to `node`. */
|
|
226
|
+
const pathOf = (node) => {
|
|
227
|
+
let depth = 0;
|
|
228
|
+
for (let n = node; n !== undefined && n.parent !== undefined; n = n.parent)
|
|
229
|
+
depth++;
|
|
230
|
+
const out = new Array(depth);
|
|
231
|
+
let i = depth - 1;
|
|
232
|
+
for (let n = node; n !== undefined && n.parent !== undefined; n = n.parent) {
|
|
233
|
+
out[i--] = n.key;
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
};
|
|
237
|
+
const applySelector = (node, selector, root, out) => {
|
|
238
|
+
const value = node.value;
|
|
239
|
+
switch (selector.kind) {
|
|
240
|
+
case 'child': {
|
|
241
|
+
if (isObject(value)) {
|
|
242
|
+
if (Object.hasOwn(value, selector.name))
|
|
243
|
+
out.push({ value: value[selector.name], parent: node, key: selector.name });
|
|
244
|
+
}
|
|
245
|
+
else if (Array.isArray(value) && /^\d+$/.test(selector.name)) {
|
|
246
|
+
const idx = Number(selector.name);
|
|
247
|
+
if (idx < value.length)
|
|
248
|
+
out.push({ value: value[idx], parent: node, key: idx });
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
case 'index': {
|
|
253
|
+
if (Array.isArray(value)) {
|
|
254
|
+
const idx = selector.index < 0 ? value.length + selector.index : selector.index;
|
|
255
|
+
if (idx >= 0 && idx < value.length)
|
|
256
|
+
out.push({ value: value[idx], parent: node, key: idx });
|
|
257
|
+
}
|
|
258
|
+
else if (isObject(value) && Object.hasOwn(value, selector.index)) {
|
|
259
|
+
out.push({ value: value[selector.index], parent: node, key: selector.index });
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
case 'wildcard': {
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
for (let idx = 0; idx < value.length; idx++)
|
|
266
|
+
out.push({ value: value[idx], parent: node, key: idx });
|
|
267
|
+
}
|
|
268
|
+
else if (isObject(value)) {
|
|
269
|
+
for (const key of Object.keys(value))
|
|
270
|
+
out.push({ value: value[key], parent: node, key });
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
case 'union': {
|
|
275
|
+
for (const name of selector.names) {
|
|
276
|
+
if (Array.isArray(value)) {
|
|
277
|
+
if (typeof name === 'number') {
|
|
278
|
+
const idx = name < 0 ? value.length + name : name;
|
|
279
|
+
if (idx >= 0 && idx < value.length)
|
|
280
|
+
out.push({ value: value[idx], parent: node, key: idx });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (isObject(value) && Object.hasOwn(value, name)) {
|
|
284
|
+
out.push({ value: value[name], parent: node, key: name });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
case 'filter': {
|
|
290
|
+
// `@parentProperty` is `node.key`; `@path` is materialized only when used.
|
|
291
|
+
const pp = node.key;
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
for (let idx = 0; idx < value.length; idx++) {
|
|
294
|
+
const child = { value: value[idx], parent: node, key: idx };
|
|
295
|
+
const path = selector.usesPath ? pathOf(child) : EMPTY_PATH;
|
|
296
|
+
if (selector.test(value[idx], idx, value, root, path, pp))
|
|
297
|
+
out.push(child);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (isObject(value)) {
|
|
301
|
+
for (const key of Object.keys(value)) {
|
|
302
|
+
const child = { value: value[key], parent: node, key };
|
|
303
|
+
const path = selector.usesPath ? pathOf(child) : EMPTY_PATH;
|
|
304
|
+
if (selector.test(value[key], key, value, root, path, pp))
|
|
305
|
+
out.push(child);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
case 'parent': {
|
|
311
|
+
if (node.parent !== undefined)
|
|
312
|
+
out.push(node.parent);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
case 'keys': {
|
|
316
|
+
if (node.parent === undefined)
|
|
317
|
+
return;
|
|
318
|
+
// The selected value is the node's own key, but it occupies the same path.
|
|
319
|
+
out.push({ value: node.key, parent: node.parent, key: node.key });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
/** Visits `node` and every descendant (preorder), invoking `visit` on each. */
|
|
325
|
+
const walkDescendants = (node, visit) => {
|
|
326
|
+
visit(node);
|
|
327
|
+
const value = node.value;
|
|
328
|
+
if (Array.isArray(value)) {
|
|
329
|
+
for (let idx = 0; idx < value.length; idx++)
|
|
330
|
+
walkDescendants({ value: value[idx], parent: node, key: idx }, visit);
|
|
331
|
+
}
|
|
332
|
+
else if (isObject(value)) {
|
|
333
|
+
for (const key of Object.keys(value))
|
|
334
|
+
walkDescendants({ value: value[key], parent: node, key }, visit);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
/** Applies a list of steps to an existing set of nodes. */
|
|
338
|
+
const applySteps = (root, initial, steps) => {
|
|
339
|
+
let current = initial;
|
|
340
|
+
for (const step of steps) {
|
|
341
|
+
const next = [];
|
|
342
|
+
if (step.recursive) {
|
|
343
|
+
for (const node of current)
|
|
344
|
+
walkDescendants(node, (n) => applySelector(n, step.selector, root, next));
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
for (const node of current)
|
|
348
|
+
applySelector(node, step.selector, root, next);
|
|
349
|
+
}
|
|
350
|
+
current = next;
|
|
351
|
+
}
|
|
352
|
+
return current;
|
|
353
|
+
};
|
|
354
|
+
const runSteps = (root, steps) => applySteps(root, [{ value: root, parent: undefined, key: undefined }], steps);
|
|
355
|
+
const toMatches = (nodes) => {
|
|
356
|
+
const out = new Array(nodes.length);
|
|
357
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
358
|
+
const node = nodes[i];
|
|
359
|
+
out[i] = { value: node.value, path: normalizePath(pathOf(node)) };
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
};
|
|
363
|
+
/** Evaluates a pre-compiled path against `data`. */
|
|
364
|
+
export const queryCompiled = (data, compiled) => {
|
|
365
|
+
if (data === null || data === undefined)
|
|
366
|
+
return [];
|
|
367
|
+
return toMatches(runSteps(data, compiled.steps));
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Evaluates many pre-compiled paths against `data`, sharing a *single* recursive
|
|
371
|
+
* descent of the tree across every `$..`-rooted path. The ruleset has ~16
|
|
372
|
+
* descent `given`s; walking the (post-deref, ~60k-node) tree once and testing
|
|
373
|
+
* each path's first selector at every node — instead of one full traversal per
|
|
374
|
+
* path — is the dominant rule-run speedup on large specs. Non-recursive paths
|
|
375
|
+
* are evaluated directly (cheap, no descent).
|
|
376
|
+
*
|
|
377
|
+
* Returns one match array per input path, index-aligned with `compiled`.
|
|
378
|
+
*/
|
|
379
|
+
export const queryMany = (data, compiled) => {
|
|
380
|
+
const out = new Array(compiled.length);
|
|
381
|
+
if (data === null || data === undefined) {
|
|
382
|
+
for (let i = 0; i < compiled.length; i++)
|
|
383
|
+
out[i] = [];
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
const recursive = [];
|
|
387
|
+
for (let i = 0; i < compiled.length; i++) {
|
|
388
|
+
const c = compiled[i];
|
|
389
|
+
const first = c.steps[0];
|
|
390
|
+
if (first?.recursive)
|
|
391
|
+
recursive.push(i);
|
|
392
|
+
else
|
|
393
|
+
out[i] = queryCompiled(data, c);
|
|
394
|
+
}
|
|
395
|
+
if (recursive.length > 0) {
|
|
396
|
+
// Walk the whole tree exactly once, applying every descent path's first
|
|
397
|
+
// selector at each visited node in the same pass (fused descent), so the
|
|
398
|
+
// ~15k-node resolved tree is traversed a single time rather than once per
|
|
399
|
+
// `$..` given. Each path's surviving seeds then run its remaining steps.
|
|
400
|
+
const firsts = recursive.map((i) => compiled[i].steps[0]);
|
|
401
|
+
const seeds = recursive.map(() => []);
|
|
402
|
+
const root = { value: data, parent: undefined, key: undefined };
|
|
403
|
+
walkDescendants(root, (node) => {
|
|
404
|
+
for (let r = 0; r < recursive.length; r++)
|
|
405
|
+
applySelector(node, firsts[r].selector, data, seeds[r]);
|
|
406
|
+
});
|
|
407
|
+
for (let r = 0; r < recursive.length; r++) {
|
|
408
|
+
const c = compiled[recursive[r]];
|
|
409
|
+
out[recursive[r]] = toMatches(applySteps(data, seeds[r], c.steps.slice(1)));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
};
|
|
414
|
+
/** Runs a JSONPath expression and returns each match with its concrete path. */
|
|
415
|
+
export const query = (data, expression) => queryCompiled(data, compileQuery(expression));
|