@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/dist/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
4
|
+
import { createRuleset as createCoreRuleset, lintWithResult, } from './core/index.js';
|
|
5
|
+
import { createFixPlugin, FIX_PLUGIN_NAME } from './fix/index.js';
|
|
6
|
+
import { builtinFunctions } from './functions/index.js';
|
|
7
|
+
import { parseWithPointers } from './parsers/index.js';
|
|
8
|
+
// Re-export the engine, built-in functions, and fix subsystem as the package's
|
|
9
|
+
// public API. `export *` from `./core` also provides a low-level `createRuleset`,
|
|
10
|
+
// but the higher-level wrapper defined below (which layers in the built-in
|
|
11
|
+
// functions and file/package `extends` resolution) is the local export and wins.
|
|
12
|
+
// Rendering findings is a consumer concern: `lintDocument` returns structured
|
|
13
|
+
// `IDiagnostic[]`, and the caller decides how to display or serialize them.
|
|
14
|
+
export * from './core/index.js';
|
|
15
|
+
export * from './fix/index.js';
|
|
16
|
+
export * from './functions/index.js';
|
|
17
|
+
export { detectFormat, parseWithPointers } from './parsers/index.js';
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
/** Loads a ruleset definition from a file path by extension (YAML/JSON parsed, JS/CJS/MJS required). */
|
|
20
|
+
const loadRulesetFile = (file) => {
|
|
21
|
+
if (/\.(ya?ml|json)$/i.test(file)) {
|
|
22
|
+
return parseWithPointers(readFileSync(file, 'utf8')).data;
|
|
23
|
+
}
|
|
24
|
+
const module = require(file);
|
|
25
|
+
return (module.default ?? module);
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Resolves an `extends` reference to a ruleset definition. Supports:
|
|
29
|
+
* - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
|
|
30
|
+
* - npm package specifiers (resolved from `basePath`), including subpaths.
|
|
31
|
+
*
|
|
32
|
+
* The engine ships no named built-in rulesets, so every string `extends` target
|
|
33
|
+
* is a file path or an npm package.
|
|
34
|
+
*/
|
|
35
|
+
export const resolveNamedRuleset = (name, basePath = process.cwd()) => {
|
|
36
|
+
if (name.startsWith('.') || isAbsolute(name)) {
|
|
37
|
+
const file = resolvePath(basePath, name);
|
|
38
|
+
return { definition: loadRulesetFile(file), basePath: dirname(file) };
|
|
39
|
+
}
|
|
40
|
+
let file;
|
|
41
|
+
try {
|
|
42
|
+
file = require.resolve(name, { paths: [basePath] });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error(`Cannot resolve extended ruleset "${name}" from ${basePath}`);
|
|
46
|
+
}
|
|
47
|
+
return { definition: loadRulesetFile(file), basePath: dirname(file) };
|
|
48
|
+
};
|
|
49
|
+
/** Loads a single custom function module (`<dir>/<name>.{js,cjs,mjs}` or a bare path). */
|
|
50
|
+
const loadFunctionByName = (basePath, dir, name) => {
|
|
51
|
+
const baseFile = resolvePath(basePath, dir, name);
|
|
52
|
+
for (const candidate of [baseFile, `${baseFile}.js`, `${baseFile}.cjs`, `${baseFile}.mjs`]) {
|
|
53
|
+
try {
|
|
54
|
+
const resolvedFile = require.resolve(candidate);
|
|
55
|
+
const module = require(resolvedFile);
|
|
56
|
+
const fn = module.default ?? module;
|
|
57
|
+
if (typeof fn !== 'function')
|
|
58
|
+
throw new Error(`"${name}" did not export a function`);
|
|
59
|
+
return fn;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code !== 'MODULE_NOT_FOUND')
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Cannot resolve custom function "${name}" from ${resolvePath(basePath, dir)}`);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Walks a ruleset definition (and its string `extends`) collecting custom
|
|
70
|
+
* functions declared via `functions` / `functionsDir`, each loaded relative to
|
|
71
|
+
* the directory of the ruleset that declared it. YAML/JSON rulesets reference
|
|
72
|
+
* functions by name; JS rulesets can instead pass direct references in `then`.
|
|
73
|
+
*/
|
|
74
|
+
const collectCustomFunctions = (definition, basePath, into, seen) => {
|
|
75
|
+
if (seen.has(definition))
|
|
76
|
+
return;
|
|
77
|
+
seen.add(definition);
|
|
78
|
+
if (definition.extends) {
|
|
79
|
+
const entries = Array.isArray(definition.extends) ? definition.extends : [definition.extends];
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const target = Array.isArray(entry) ? entry[0] : entry;
|
|
82
|
+
if (typeof target === 'string') {
|
|
83
|
+
const resolved = resolveNamedRuleset(target, basePath);
|
|
84
|
+
collectCustomFunctions(resolved.definition, resolved.basePath, into, seen);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
collectCustomFunctions(target, basePath, into, seen);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(definition.functions)) {
|
|
92
|
+
const dir = definition.functionsDir ?? 'functions';
|
|
93
|
+
for (const name of definition.functions)
|
|
94
|
+
into[name] = loadFunctionByName(basePath, dir, name);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Builds a runnable {@link Ruleset} from a ruleset definition, layering the
|
|
99
|
+
* built-in functions (plus any custom ones the definition declares via
|
|
100
|
+
* `functions` / `functionsDir`) over the core engine and wiring up `extends`
|
|
101
|
+
* resolution against files and npm packages. With no definition it produces an
|
|
102
|
+
* empty ruleset (no rules run).
|
|
103
|
+
*/
|
|
104
|
+
export const createRuleset = (definition, basePath) => {
|
|
105
|
+
const resolved = definition ?? {};
|
|
106
|
+
// Custom functions referenced by name (YAML/JSON rulesets) are loaded relative
|
|
107
|
+
// to the declaring ruleset's directory and layered over the built-ins.
|
|
108
|
+
let functions = builtinFunctions;
|
|
109
|
+
const custom = {};
|
|
110
|
+
collectCustomFunctions(resolved, basePath ?? process.cwd(), custom, new Set());
|
|
111
|
+
if (Object.keys(custom).length > 0)
|
|
112
|
+
functions = { ...builtinFunctions, ...custom };
|
|
113
|
+
return createCoreRuleset(resolved, {
|
|
114
|
+
functions,
|
|
115
|
+
resolve: resolveNamedRuleset,
|
|
116
|
+
...(basePath !== undefined ? { basePath } : {}),
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Lints a JSON/YAML `input` end to end: parses with source maps and applies the
|
|
121
|
+
* ruleset. Returns just the findings; use {@link lintDocumentWithResult} for the
|
|
122
|
+
* full result.
|
|
123
|
+
*/
|
|
124
|
+
export const lintDocument = async (input, options = {}) => (await lintDocumentWithResult(input, options)).diagnostics;
|
|
125
|
+
/**
|
|
126
|
+
* Like {@link lintDocument}, but returns the full {@link ILintResult} — including
|
|
127
|
+
* anything the configured `plugins` produced (e.g. the auto-fix plugin's
|
|
128
|
+
* rewritten `output`).
|
|
129
|
+
*/
|
|
130
|
+
export const lintDocumentWithResult = async (input, options = {}) => {
|
|
131
|
+
const { ruleset: rulesetDefinition, rulesetBasePath, resolve, plugins, ...documentOptions } = options;
|
|
132
|
+
const ruleset = createRuleset(rulesetDefinition, rulesetBasePath);
|
|
133
|
+
return lintWithResult(input, {
|
|
134
|
+
...documentOptions,
|
|
135
|
+
ruleset,
|
|
136
|
+
...(resolve ? { resolve } : {}),
|
|
137
|
+
...(plugins ? { plugins } : {}),
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
// One fix pass can unblock the next, so we lint-and-fix to a fixpoint. The cap is
|
|
141
|
+
// a safety net against a fixer that oscillates rather than converging — in
|
|
142
|
+
// practice a couple of passes is plenty.
|
|
143
|
+
const MAX_FIX_PASSES = 10;
|
|
144
|
+
/**
|
|
145
|
+
* Lints a document and applies the supplied `fixers` repeatedly until the
|
|
146
|
+
* document stops changing (or {@link MAX_FIX_PASSES} is reached), then re-lints
|
|
147
|
+
* so `remaining` reflects the fixed document. A one-call convenience over
|
|
148
|
+
* {@link lintDocumentWithResult} + `createFixPlugin`. With no `fixers` this is a
|
|
149
|
+
* no-op that just returns the findings.
|
|
150
|
+
*/
|
|
151
|
+
export const fixDocument = async (input, options = {}) => {
|
|
152
|
+
const { fixers = {}, safeOnly, ...lintOptions } = options;
|
|
153
|
+
const plugin = createFixPlugin(fixers, { safeOnly: safeOnly !== false });
|
|
154
|
+
let current = input;
|
|
155
|
+
const applied = [];
|
|
156
|
+
for (let pass = 0; pass < MAX_FIX_PASSES; pass++) {
|
|
157
|
+
const result = await lintDocumentWithResult(current, { ...lintOptions, plugins: [plugin] });
|
|
158
|
+
// No rewrite, or a rewrite that matches what we already have, means we have converged.
|
|
159
|
+
if (result.output === undefined || result.output === current)
|
|
160
|
+
break;
|
|
161
|
+
current = result.output;
|
|
162
|
+
const data = result.pluginData[FIX_PLUGIN_NAME];
|
|
163
|
+
if (data)
|
|
164
|
+
applied.push(...data.applied);
|
|
165
|
+
}
|
|
166
|
+
const remaining = await lintDocument(current, lintOptions);
|
|
167
|
+
return { output: current, fixed: applied.length > 0, remaining, applied };
|
|
168
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ParserFormat } from './index.js';
|
|
2
|
+
import type { JsonPath } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A single structural edit against a parsed document, expressed in terms of JSON
|
|
5
|
+
* paths and values rather than raw character offsets. {@link applyEditOps} lowers
|
|
6
|
+
* each op to a minimal text edit so that untouched parts of the source — including
|
|
7
|
+
* comments, key order, and quoting — keep their original formatting.
|
|
8
|
+
*/
|
|
9
|
+
export type EditOp =
|
|
10
|
+
/** Replace the scalar value at `path` (the existing quoting style is preserved). */
|
|
11
|
+
{
|
|
12
|
+
op: 'setValue';
|
|
13
|
+
path: JsonPath;
|
|
14
|
+
value: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Remove the object property at `path` (the last segment is the key). */
|
|
17
|
+
| {
|
|
18
|
+
op: 'removeProperty';
|
|
19
|
+
path: JsonPath;
|
|
20
|
+
}
|
|
21
|
+
/** Rename the object property at `path` (the last segment is the current key). */
|
|
22
|
+
| {
|
|
23
|
+
op: 'renameProperty';
|
|
24
|
+
path: JsonPath;
|
|
25
|
+
newKey: string;
|
|
26
|
+
}
|
|
27
|
+
/** Remove the listed array indices from the array at `path`. */
|
|
28
|
+
| {
|
|
29
|
+
op: 'removeItems';
|
|
30
|
+
path: JsonPath;
|
|
31
|
+
indices: number[];
|
|
32
|
+
}
|
|
33
|
+
/** Reorder the array at `path`; `order` lists the original indices in their new order. */
|
|
34
|
+
| {
|
|
35
|
+
op: 'reorderArray';
|
|
36
|
+
path: JsonPath;
|
|
37
|
+
order: number[];
|
|
38
|
+
}
|
|
39
|
+
/** Add a `key: value` property to the object at `path` (a no-op when the key already exists). */
|
|
40
|
+
| {
|
|
41
|
+
op: 'insertProperty';
|
|
42
|
+
path: JsonPath;
|
|
43
|
+
key: string;
|
|
44
|
+
value: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** Insert `value` into the array at `path` at `index` (appended when `index` is omitted). */
|
|
47
|
+
| {
|
|
48
|
+
op: 'insertItem';
|
|
49
|
+
path: JsonPath;
|
|
50
|
+
value: unknown;
|
|
51
|
+
index?: number;
|
|
52
|
+
};
|
|
53
|
+
/** The text after applying a batch of edits, plus which ops actually changed it. */
|
|
54
|
+
export type ApplyEditOpsResult = {
|
|
55
|
+
output: string;
|
|
56
|
+
/** `changed[i]` is whether `ops[i]` altered the text (false when its path no longer resolved). */
|
|
57
|
+
changed: boolean[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Applies `ops` to `source`, reporting both the edited text and, per op, whether
|
|
61
|
+
* it actually changed anything. Each op is lowered to a minimal text edit and
|
|
62
|
+
* applied sequentially (re-parsing between ops so offsets stay valid), preserving
|
|
63
|
+
* the formatting of everything it does not touch. An op whose target path no
|
|
64
|
+
* longer resolves is a no-op rather than an error — callers use `changed` to tell
|
|
65
|
+
* a real fix from one that quietly dropped out (e.g. its node was already gone).
|
|
66
|
+
*/
|
|
67
|
+
export declare const applyEditOpsWithChanges: (source: string, format: ParserFormat, ops: EditOp[]) => ApplyEditOpsResult;
|
|
68
|
+
/** Applies `ops` to `source` and returns just the edited text. See {@link applyEditOpsWithChanges}. */
|
|
69
|
+
export declare const applyEditOps: (source: string, format: ParserFormat, ops: EditOp[]) => string;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { isMap, isScalar, isSeq, parseDocument } from '@amritk/yaml';
|
|
2
|
+
import { applyEdits, findNodeAtLocation, getNodeValue, modify, parseTree } from 'jsonc-parser';
|
|
3
|
+
const splice = (text, start, end, replacement) => text.slice(0, start) + replacement + text.slice(end);
|
|
4
|
+
/** Expands `[start, end)` to cover whole lines: back to the line start, forward past the trailing newline. */
|
|
5
|
+
const expandLine = (text, start, end) => {
|
|
6
|
+
let s = start;
|
|
7
|
+
while (s > 0 && text.charCodeAt(s - 1) !== 10)
|
|
8
|
+
s--;
|
|
9
|
+
let e = end;
|
|
10
|
+
while (e < text.length && text.charCodeAt(e) !== 10)
|
|
11
|
+
e++;
|
|
12
|
+
if (e < text.length)
|
|
13
|
+
e++;
|
|
14
|
+
return [s, e];
|
|
15
|
+
};
|
|
16
|
+
// --- YAML ------------------------------------------------------------------
|
|
17
|
+
const keyName = (key) => (isScalar(key) ? String(key.value) : String(key));
|
|
18
|
+
/** Navigates the YAML CST to the node at `path`, or `undefined` if absent. */
|
|
19
|
+
const yamlNodeAt = (root, path) => {
|
|
20
|
+
let current = root ?? undefined;
|
|
21
|
+
for (const segment of path) {
|
|
22
|
+
if (current === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (isMap(current)) {
|
|
25
|
+
const target = String(segment);
|
|
26
|
+
const pair = current.items.find((item) => keyName(item.key) === target);
|
|
27
|
+
current = pair?.value ?? undefined;
|
|
28
|
+
}
|
|
29
|
+
else if (isSeq(current)) {
|
|
30
|
+
current = current.items[Number(segment)];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return current;
|
|
37
|
+
};
|
|
38
|
+
/** Resolves the key/value CST nodes of the property at `path` (last segment = key). */
|
|
39
|
+
const yamlPairAt = (root, path) => {
|
|
40
|
+
const parent = yamlNodeAt(root, path.slice(0, -1));
|
|
41
|
+
if (!parent || !isMap(parent))
|
|
42
|
+
return {};
|
|
43
|
+
const last = String(path[path.length - 1]);
|
|
44
|
+
const pair = parent.items.find((item) => keyName(item.key) === last);
|
|
45
|
+
if (!pair)
|
|
46
|
+
return {};
|
|
47
|
+
return { key: pair.key, ...(pair.value ? { value: pair.value } : {}) };
|
|
48
|
+
};
|
|
49
|
+
/** Serializes a scalar, preserving the quoting style of the value it replaces. */
|
|
50
|
+
const yamlScalar = (value, original) => {
|
|
51
|
+
if (typeof value !== 'string')
|
|
52
|
+
return value === null ? 'null' : String(value);
|
|
53
|
+
const quote = original.charCodeAt(0);
|
|
54
|
+
if (quote === 34 /* " */)
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
if (quote === 39 /* ' */)
|
|
57
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
58
|
+
return value;
|
|
59
|
+
};
|
|
60
|
+
const yamlKey = (key, original) => {
|
|
61
|
+
const quote = original.charCodeAt(0);
|
|
62
|
+
if (quote === 34)
|
|
63
|
+
return JSON.stringify(key);
|
|
64
|
+
if (quote === 39)
|
|
65
|
+
return `'${key.replace(/'/g, "''")}'`;
|
|
66
|
+
return key;
|
|
67
|
+
};
|
|
68
|
+
// Keys made only of these characters are safe to write bare in YAML; anything
|
|
69
|
+
// else (spaces, colons, flow indicators) gets double-quoted to stay valid.
|
|
70
|
+
const SAFE_YAML_KEY = /^[\w./-]+$/;
|
|
71
|
+
/** Serializes a key for an inserted property, quoting it only when it needs to be. */
|
|
72
|
+
const yamlInsertKey = (key) => (SAFE_YAML_KEY.test(key) ? key : JSON.stringify(key));
|
|
73
|
+
/**
|
|
74
|
+
* Serializes a value for insertion. YAML is a JSON superset, so JSON output is
|
|
75
|
+
* always valid YAML: scalars become bare literals (`42`, `true`, `"text"`) and
|
|
76
|
+
* objects/arrays become inline flow (`{"a":1}`). That keeps the inserted node on
|
|
77
|
+
* one line without us having to re-implement a block serializer.
|
|
78
|
+
*/
|
|
79
|
+
const yamlInsertValue = (value) => JSON.stringify(value) ?? 'null';
|
|
80
|
+
/** Returns the leading whitespace of the line containing `offset` (its indentation). */
|
|
81
|
+
const lineIndent = (text, offset) => {
|
|
82
|
+
let start = offset;
|
|
83
|
+
while (start > 0 && text.charCodeAt(start - 1) !== 10)
|
|
84
|
+
start--;
|
|
85
|
+
let end = start;
|
|
86
|
+
while (end < text.length && (text.charCodeAt(end) === 32 || text.charCodeAt(end) === 9))
|
|
87
|
+
end++;
|
|
88
|
+
return text.slice(start, end);
|
|
89
|
+
};
|
|
90
|
+
const applyYamlOp = (text, op) => {
|
|
91
|
+
const root = parseDocument(text).contents;
|
|
92
|
+
switch (op.op) {
|
|
93
|
+
case 'setValue': {
|
|
94
|
+
const node = yamlNodeAt(root, op.path);
|
|
95
|
+
if (!node)
|
|
96
|
+
return text;
|
|
97
|
+
// Scalars keep the node's original quoting; objects/arrays are written as
|
|
98
|
+
// inline flow JSON (valid YAML) so a scalar can be widened to a collection.
|
|
99
|
+
const replacement = op.value !== null && typeof op.value === 'object'
|
|
100
|
+
? yamlInsertValue(op.value)
|
|
101
|
+
: yamlScalar(op.value, text.slice(node.start, node.end));
|
|
102
|
+
return splice(text, node.start, node.end, replacement);
|
|
103
|
+
}
|
|
104
|
+
case 'renameProperty': {
|
|
105
|
+
const { key } = yamlPairAt(root, op.path);
|
|
106
|
+
if (!key)
|
|
107
|
+
return text;
|
|
108
|
+
return splice(text, key.start, key.end, yamlKey(op.newKey, text.slice(key.start, key.end)));
|
|
109
|
+
}
|
|
110
|
+
case 'removeProperty': {
|
|
111
|
+
const parent = yamlNodeAt(root, op.path.slice(0, -1));
|
|
112
|
+
if (!parent || !isMap(parent))
|
|
113
|
+
return text;
|
|
114
|
+
const last = String(op.path[op.path.length - 1]);
|
|
115
|
+
const index = parent.items.findIndex((item) => keyName(item.key) === last);
|
|
116
|
+
if (index === -1)
|
|
117
|
+
return text;
|
|
118
|
+
const pair = parent.items[index];
|
|
119
|
+
const key = pair?.key;
|
|
120
|
+
const value = pair?.value;
|
|
121
|
+
// Flow map (`{ a: 1, b: 2 }`): excise just this member plus one adjoining
|
|
122
|
+
// comma, keeping the braces and the surviving members on their shared line.
|
|
123
|
+
if (text.charCodeAt(parent.start) === 123 /* { */) {
|
|
124
|
+
if (parent.items.length === 1)
|
|
125
|
+
return splice(text, parent.start + 1, parent.end - 1, '');
|
|
126
|
+
// Not the last member: take the comma and gap up to the next key.
|
|
127
|
+
if (index < parent.items.length - 1) {
|
|
128
|
+
const next = parent.items[index + 1]?.key;
|
|
129
|
+
return splice(text, key.start, next.start, '');
|
|
130
|
+
}
|
|
131
|
+
// Last member: take the trailing comma and gap left by the previous one.
|
|
132
|
+
const prev = parent.items[index - 1];
|
|
133
|
+
const prevEnd = (prev?.value ?? prev?.key).end;
|
|
134
|
+
return splice(text, prevEnd, (value ?? key).end, '');
|
|
135
|
+
}
|
|
136
|
+
// Block map: drop the whole line(s) the property occupies.
|
|
137
|
+
const [start, end] = expandLine(text, key.start, value ? value.end : key.end);
|
|
138
|
+
return splice(text, start, end, '');
|
|
139
|
+
}
|
|
140
|
+
case 'removeItems': {
|
|
141
|
+
const seq = yamlNodeAt(root, op.path);
|
|
142
|
+
if (!seq || !isSeq(seq))
|
|
143
|
+
return text;
|
|
144
|
+
const removed = new Set(op.indices);
|
|
145
|
+
return rewriteYamlSeq(text, seq, seq.items.filter((_, index) => !removed.has(index)));
|
|
146
|
+
}
|
|
147
|
+
case 'reorderArray': {
|
|
148
|
+
const seq = yamlNodeAt(root, op.path);
|
|
149
|
+
if (!seq || !isSeq(seq) || seq.items.length === 0)
|
|
150
|
+
return text;
|
|
151
|
+
const reordered = op.order.map((index) => seq.items[index]).filter((item) => item != null);
|
|
152
|
+
return rewriteYamlSeq(text, seq, reordered);
|
|
153
|
+
}
|
|
154
|
+
case 'insertProperty': {
|
|
155
|
+
const parent = yamlNodeAt(root, op.path);
|
|
156
|
+
if (!parent || !isMap(parent))
|
|
157
|
+
return text;
|
|
158
|
+
// Inserting is additive only; if the key is already there we leave it alone.
|
|
159
|
+
if (parent.items.some((item) => keyName(item.key) === op.key))
|
|
160
|
+
return text;
|
|
161
|
+
const pair = `${yamlInsertKey(op.key)}: ${yamlInsertValue(op.value)}`;
|
|
162
|
+
if (text.charCodeAt(parent.start) === 123 /* { */) {
|
|
163
|
+
const last = parent.items[parent.items.length - 1];
|
|
164
|
+
// Append right after the last entry (keeping the brace's own spacing); an
|
|
165
|
+
// empty `{}` gets the pair tucked just inside the opening brace.
|
|
166
|
+
if (last) {
|
|
167
|
+
const lastEnd = (last.value ?? last.key).end;
|
|
168
|
+
return splice(text, lastEnd, lastEnd, `, ${pair}`);
|
|
169
|
+
}
|
|
170
|
+
return splice(text, parent.start + 1, parent.start + 1, pair);
|
|
171
|
+
}
|
|
172
|
+
// Block map: mirror the existing keys' indentation and add a line after the last one.
|
|
173
|
+
const last = parent.items[parent.items.length - 1];
|
|
174
|
+
const lastKey = last?.key;
|
|
175
|
+
if (!lastKey)
|
|
176
|
+
return text;
|
|
177
|
+
const indent = lineIndent(text, lastKey.start);
|
|
178
|
+
const [, end] = expandLine(text, lastKey.start, (last?.value ?? lastKey).end);
|
|
179
|
+
const lead = end > 0 && text.charCodeAt(end - 1) !== 10 ? '\n' : '';
|
|
180
|
+
return splice(text, end, end, `${lead}${indent}${pair}\n`);
|
|
181
|
+
}
|
|
182
|
+
case 'insertItem': {
|
|
183
|
+
const seq = yamlNodeAt(root, op.path);
|
|
184
|
+
if (!seq || !isSeq(seq))
|
|
185
|
+
return text;
|
|
186
|
+
const value = yamlInsertValue(op.value);
|
|
187
|
+
if (text.charCodeAt(seq.start) === 91 /* [ */) {
|
|
188
|
+
const items = seq.items.map((item) => text.slice(item.start, item.end));
|
|
189
|
+
items.splice(clampIndex(op.index, items.length), 0, value);
|
|
190
|
+
return splice(text, seq.start + 1, seq.end - 1, items.join(', '));
|
|
191
|
+
}
|
|
192
|
+
// Block sequence: we need an existing `- item` line to mirror its indentation and style.
|
|
193
|
+
if (seq.items.length === 0)
|
|
194
|
+
return text;
|
|
195
|
+
const blocks = seq.items.map((item) => expandLine(text, item.start, item.end));
|
|
196
|
+
const regionStart = blocks[0][0];
|
|
197
|
+
const regionEnd = blocks[blocks.length - 1][1];
|
|
198
|
+
const lines = seq.items.map((_, index) => text.slice(...blocks[index]));
|
|
199
|
+
lines.splice(clampIndex(op.index, lines.length), 0, `${lineIndent(text, regionStart)}- ${value}\n`);
|
|
200
|
+
return splice(text, regionStart, regionEnd, lines.join(''));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
/** Clamps an optional insertion index into `[0, length]`, defaulting to an append. */
|
|
205
|
+
const clampIndex = (index, length) => Math.max(0, Math.min(index ?? length, length));
|
|
206
|
+
/**
|
|
207
|
+
* Rewrites a YAML sequence to contain exactly `newItems` (a subset and/or
|
|
208
|
+
* reordering of the original nodes), preserving each kept item's own text. Flow
|
|
209
|
+
* sequences (`[a, b]`) are rebuilt inside their brackets; block sequences (one
|
|
210
|
+
* `- item` per line) are rebuilt from their whole-line spans.
|
|
211
|
+
*/
|
|
212
|
+
const rewriteYamlSeq = (text, seq, newItems) => {
|
|
213
|
+
const isFlow = text.charCodeAt(seq.start) === 91; /* [ */
|
|
214
|
+
if (isFlow) {
|
|
215
|
+
const inner = newItems.map((item) => text.slice(item.start, item.end)).join(', ');
|
|
216
|
+
return splice(text, seq.start + 1, seq.end - 1, inner);
|
|
217
|
+
}
|
|
218
|
+
if (seq.items.length === 0)
|
|
219
|
+
return text;
|
|
220
|
+
const blocks = seq.items.map((item) => expandLine(text, item.start, item.end));
|
|
221
|
+
const regionStart = blocks[0][0];
|
|
222
|
+
const regionEnd = blocks[blocks.length - 1][1];
|
|
223
|
+
const blockText = new Map(seq.items.map((item, index) => [item, text.slice(...blocks[index])]));
|
|
224
|
+
const rebuilt = newItems.map((item) => blockText.get(item) ?? '').join('');
|
|
225
|
+
return splice(text, regionStart, regionEnd, rebuilt);
|
|
226
|
+
};
|
|
227
|
+
// --- JSON ------------------------------------------------------------------
|
|
228
|
+
const JSON_FORMAT = { insertSpaces: true, tabSize: 2, eol: '\n' };
|
|
229
|
+
/**
|
|
230
|
+
* jsonc-parser keys path segments by JS type — strings index objects, numbers
|
|
231
|
+
* index arrays — but a finding's path carries numeric-like object keys (e.g. a
|
|
232
|
+
* `"200"` response) as plain numbers. Walk the tree and coerce each segment to
|
|
233
|
+
* the type its parent expects, so both keys and indices resolve (and `modify`
|
|
234
|
+
* doesn't mistake a `"200"` key for an array index and throw).
|
|
235
|
+
*/
|
|
236
|
+
const normalizeJsonPath = (root, path) => {
|
|
237
|
+
const result = [];
|
|
238
|
+
let node = root;
|
|
239
|
+
for (const segment of path) {
|
|
240
|
+
const normalized = node?.type === 'array' ? Number(segment) : String(segment);
|
|
241
|
+
result.push(normalized);
|
|
242
|
+
node = node ? findNodeAtLocation(node, [normalized]) : undefined;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
const applyJsonOp = (text, op) => {
|
|
247
|
+
const root = parseTree(text);
|
|
248
|
+
if (!root)
|
|
249
|
+
return text;
|
|
250
|
+
const path = normalizeJsonPath(root, op.path);
|
|
251
|
+
switch (op.op) {
|
|
252
|
+
case 'setValue':
|
|
253
|
+
return applyEdits(text, modify(text, path, op.value, { formattingOptions: JSON_FORMAT }));
|
|
254
|
+
case 'removeProperty':
|
|
255
|
+
return applyEdits(text, modify(text, path, undefined, { formattingOptions: JSON_FORMAT }));
|
|
256
|
+
case 'renameProperty': {
|
|
257
|
+
const valueNode = findNodeAtLocation(root, path);
|
|
258
|
+
const keyNode = valueNode?.parent?.children?.[0];
|
|
259
|
+
if (!keyNode)
|
|
260
|
+
return text;
|
|
261
|
+
return splice(text, keyNode.offset, keyNode.offset + keyNode.length, JSON.stringify(op.newKey));
|
|
262
|
+
}
|
|
263
|
+
case 'removeItems': {
|
|
264
|
+
// jsonc-parser's per-index array removal is unreliable, so rewrite the
|
|
265
|
+
// whole array with the kept elements instead.
|
|
266
|
+
const node = findNodeAtLocation(root, path);
|
|
267
|
+
if (!node)
|
|
268
|
+
return text;
|
|
269
|
+
const removed = new Set(op.indices);
|
|
270
|
+
const kept = getNodeValue(node).filter((_, index) => !removed.has(index));
|
|
271
|
+
return applyEdits(text, modify(text, path, kept, { formattingOptions: JSON_FORMAT }));
|
|
272
|
+
}
|
|
273
|
+
case 'reorderArray': {
|
|
274
|
+
const node = findNodeAtLocation(root, path);
|
|
275
|
+
if (!node)
|
|
276
|
+
return text;
|
|
277
|
+
const array = getNodeValue(node);
|
|
278
|
+
const reordered = op.order.map((index) => array[index]);
|
|
279
|
+
return applyEdits(text, modify(text, path, reordered, { formattingOptions: JSON_FORMAT }));
|
|
280
|
+
}
|
|
281
|
+
case 'insertProperty': {
|
|
282
|
+
const node = findNodeAtLocation(root, path);
|
|
283
|
+
if (!node || node.type !== 'object')
|
|
284
|
+
return text;
|
|
285
|
+
// Additive only: writing to an existing key would replace its value, so bail.
|
|
286
|
+
if (node.children?.some((property) => property.children?.[0]?.value === op.key))
|
|
287
|
+
return text;
|
|
288
|
+
return applyEdits(text, modify(text, [...path, op.key], op.value, { formattingOptions: JSON_FORMAT }));
|
|
289
|
+
}
|
|
290
|
+
case 'insertItem': {
|
|
291
|
+
const node = findNodeAtLocation(root, path);
|
|
292
|
+
if (!node || node.type !== 'array')
|
|
293
|
+
return text;
|
|
294
|
+
const index = clampIndex(op.index, node.children?.length ?? 0);
|
|
295
|
+
return applyEdits(text, modify(text, [...path, index], op.value, { formattingOptions: JSON_FORMAT, isArrayInsertion: true }));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Applies `ops` to `source`, reporting both the edited text and, per op, whether
|
|
301
|
+
* it actually changed anything. Each op is lowered to a minimal text edit and
|
|
302
|
+
* applied sequentially (re-parsing between ops so offsets stay valid), preserving
|
|
303
|
+
* the formatting of everything it does not touch. An op whose target path no
|
|
304
|
+
* longer resolves is a no-op rather than an error — callers use `changed` to tell
|
|
305
|
+
* a real fix from one that quietly dropped out (e.g. its node was already gone).
|
|
306
|
+
*/
|
|
307
|
+
export const applyEditOpsWithChanges = (source, format, ops) => {
|
|
308
|
+
let text = source;
|
|
309
|
+
const changed = [];
|
|
310
|
+
for (const op of ops) {
|
|
311
|
+
const before = text;
|
|
312
|
+
// An op that can't be lowered (an unresolved path, a key the underlying
|
|
313
|
+
// editor rejects) must be a no-op, never a thrown error that aborts the
|
|
314
|
+
// whole batch — one bad edit should not drop every other fix.
|
|
315
|
+
try {
|
|
316
|
+
text = format === 'json' ? applyJsonOp(text, op) : applyYamlOp(text, op);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
text = before;
|
|
320
|
+
}
|
|
321
|
+
changed.push(text !== before);
|
|
322
|
+
}
|
|
323
|
+
return { output: text, changed };
|
|
324
|
+
};
|
|
325
|
+
/** Applies `ops` to `source` and returns just the edited text. See {@link applyEditOpsWithChanges}. */
|
|
326
|
+
export const applyEditOps = (source, format, ops) => applyEditOpsWithChanges(source, format, ops).output;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { type ApplyEditOpsResult, applyEditOps, applyEditOpsWithChanges, type EditOp } from './edit-model.js';
|
|
2
|
+
export { parseJson } from './json.js';
|
|
3
|
+
export { createLineMap, type LineMap } from './lines.js';
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
export { parseYaml } from './yaml.js';
|
|
6
|
+
import type { IParseResult, IParserOptions } from './types.js';
|
|
7
|
+
/** Which concrete parser a document is routed to. */
|
|
8
|
+
export type ParserFormat = 'yaml' | 'json';
|
|
9
|
+
/** Guesses the format from the first non-whitespace character (`{`/`[` ⇒ JSON, else YAML). */
|
|
10
|
+
export declare const detectFormat: (source: string) => ParserFormat;
|
|
11
|
+
/**
|
|
12
|
+
* Parses a document with source maps. YAML is a JSON superset, so the YAML
|
|
13
|
+
* parser is the default and handles both; the JSON parser is used when a strict
|
|
14
|
+
* JSON document is detected or requested (it reports JSON-specific errors).
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseWithPointers: <T = unknown>(source: string, options?: IParserOptions & {
|
|
17
|
+
format?: ParserFormat;
|
|
18
|
+
}) => IParseResult<T>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { applyEditOps, applyEditOpsWithChanges } from './edit-model.js';
|
|
2
|
+
export { parseJson } from './json.js';
|
|
3
|
+
export { createLineMap } from './lines.js';
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
export { parseYaml } from './yaml.js';
|
|
6
|
+
import { parseJson } from './json.js';
|
|
7
|
+
import { parseYaml } from './yaml.js';
|
|
8
|
+
/** Guesses the format from the first non-whitespace character (`{`/`[` ⇒ JSON, else YAML). */
|
|
9
|
+
export const detectFormat = (source) => {
|
|
10
|
+
const trimmed = source.trimStart();
|
|
11
|
+
return trimmed.startsWith('{') || trimmed.startsWith('[') ? 'json' : 'yaml';
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Parses a document with source maps. YAML is a JSON superset, so the YAML
|
|
15
|
+
* parser is the default and handles both; the JSON parser is used when a strict
|
|
16
|
+
* JSON document is detected or requested (it reports JSON-specific errors).
|
|
17
|
+
*/
|
|
18
|
+
export const parseWithPointers = (source, options = {}) => {
|
|
19
|
+
const format = options.format ?? detectFormat(source);
|
|
20
|
+
return format === 'json' ? parseJson(source) : parseYaml(source, options);
|
|
21
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { findNodeAtLocation, getNodeValue, parseTree, printParseErrorCode } from 'jsonc-parser';
|
|
2
|
+
import { createLineMap } from './lines.js';
|
|
3
|
+
import { DiagnosticSeverity } from './types.js';
|
|
4
|
+
/** Parses strict JSON into data plus a source map, reporting JSON-specific syntax errors. */
|
|
5
|
+
export const parseJson = (source) => {
|
|
6
|
+
const lineMap = createLineMap(source);
|
|
7
|
+
const errors = [];
|
|
8
|
+
const root = parseTree(source, errors, { allowTrailingComma: false, disallowComments: true });
|
|
9
|
+
const data = (root ? getNodeValue(root) : undefined);
|
|
10
|
+
const diagnostics = errors.map((err) => ({
|
|
11
|
+
message: printParseErrorCode(err.error),
|
|
12
|
+
severity: DiagnosticSeverity.Error,
|
|
13
|
+
range: {
|
|
14
|
+
start: lineMap.positionAt(err.offset),
|
|
15
|
+
end: lineMap.positionAt(err.offset + err.length),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
const getLocationForJsonPath = (path, closest = false) => {
|
|
19
|
+
if (!root)
|
|
20
|
+
return undefined;
|
|
21
|
+
const p = path.slice();
|
|
22
|
+
while (true) {
|
|
23
|
+
const node = findNodeAtLocation(root, p);
|
|
24
|
+
if (node) {
|
|
25
|
+
return {
|
|
26
|
+
range: {
|
|
27
|
+
start: lineMap.positionAt(node.offset),
|
|
28
|
+
end: lineMap.positionAt(node.offset + node.length),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (!closest || p.length === 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
p.pop();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return { data, diagnostics, getLocationForJsonPath };
|
|
38
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IPosition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves byte/char offsets to `{ line, character }` positions. Lines and
|
|
4
|
+
* characters are zero-based, matching LSP / Linter conventions.
|
|
5
|
+
*/
|
|
6
|
+
export type LineMap = {
|
|
7
|
+
positionAt(offset: number): IPosition;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Builds a {@link LineMap} for `source`. The line-start offsets are precomputed
|
|
11
|
+
* once so each `positionAt` lookup is a binary search rather than a re-scan.
|
|
12
|
+
*/
|
|
13
|
+
export declare const createLineMap: (source: string) => LineMap;
|