@amritk/lint 0.7.1 → 0.8.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/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createBoundedCache, createRuleset as createCoreRuleset, lintWithResult
2
2
  import { createFixPlugin, FIX_PLUGIN_NAME } from "./fix/index.js";
3
3
  import { builtinFunctions } from "./functions/index.js";
4
4
  import { collectCustomFunctions, resolveRulesetFile } from "./ruleset-files.js";
5
- import { compileQuery, createDocument, createLinter, detectFormats, globToRegExp, lint, lintWithResult as lintWithResult2, matchesGlob, pointerToPath, query, queryCompiled, queryMany, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, runPlugins, validateRuleset } from "./core/index.js";
5
+ import { compileQuery, createDocument, createDocumentFromData, createLinter, detectFormats, globToRegExp, lint, lintWithResult as lintWithResult2, matchesGlob, pointerToPath, query, queryCompiled, queryMany, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, runPlugins, validateRuleset } from "./core/index.js";
6
6
  import { applyFixes, createFixPlugin as createFixPlugin2, FIX_PLUGIN_NAME as FIX_PLUGIN_NAME2 } from "./fix/index.js";
7
7
  import { alphabetical, builtinFunctions as builtinFunctions2, casing, defined, enumeration, falsy, length, or, pattern, schema, truthy, typedEnum, undefinedFn, unreferencedReusableObject, xor } from "./functions/index.js";
8
8
  import { detectFormat, parseWithPointers } from "./parsers/index.js";
@@ -115,6 +115,7 @@ export {
115
115
  casing,
116
116
  compileQuery,
117
117
  createDocument,
118
+ createDocumentFromData,
118
119
  createFixPlugin2 as createFixPlugin,
119
120
  createLinter,
120
121
  createRuleset,
@@ -1,23 +1,27 @@
1
1
  const MAX_NESTING_DEPTH = 1e3;
2
2
  const findExcessiveNesting = (source, limit = MAX_NESTING_DEPTH) => {
3
+ const length = source.length;
3
4
  let depth = 0;
4
- let inString = false;
5
- for (let i = 0; i < source.length; i++) {
6
- const ch = source[i];
7
- if (inString) {
8
- if (ch === "\\")
9
- i++;
10
- else if (ch === '"')
11
- inString = false;
12
- continue;
13
- }
14
- if (ch === '"')
15
- inString = true;
16
- else if (ch === "{" || ch === "[") {
5
+ for (let i = 0; i < length; i++) {
6
+ const code = source.charCodeAt(i);
7
+ if (code === 34) {
8
+ let close = source.indexOf('"', i + 1);
9
+ while (close !== -1) {
10
+ let slashes = 0;
11
+ for (let j = close - 1; source.charCodeAt(j) === 92; j--)
12
+ slashes++;
13
+ if ((slashes & 1) === 0)
14
+ break;
15
+ close = source.indexOf('"', close + 1);
16
+ }
17
+ if (close === -1)
18
+ return -1;
19
+ i = close;
20
+ } else if (code === 123 || code === 91) {
17
21
  depth++;
18
22
  if (depth > limit)
19
23
  return i;
20
- } else if (ch === "}" || ch === "]")
24
+ } else if (code === 125 || code === 93)
21
25
  depth--;
22
26
  }
23
27
  return -1;
@@ -1,6 +1,7 @@
1
1
  export { findExcessiveNesting, MAX_NESTING_DEPTH } from './depth.js';
2
2
  export { type ApplyEditOpsResult, applyEditOps, applyEditOpsWithChanges, type EditOp } from './edit-model.js';
3
3
  export { parseJson } from './json.js';
4
+ export { createJsonLocator } from './json-locator.js';
4
5
  export { createLineMap, type LineMap } from './lines.js';
5
6
  export { DiagnosticSeverity, type IDiagnostic, type ILocation, type IParseResult, type IParserOptions, type IPosition, type IRange, type JsonPath, } from './types.js';
6
7
  export { parseYaml } from './yaml.js';
@@ -1,6 +1,7 @@
1
1
  import { findExcessiveNesting, MAX_NESTING_DEPTH } from "./depth.js";
2
2
  import { applyEditOps, applyEditOpsWithChanges } from "./edit-model.js";
3
3
  import { parseJson } from "./json.js";
4
+ import { createJsonLocator } from "./json-locator.js";
4
5
  import { createLineMap } from "./lines.js";
5
6
  import { DiagnosticSeverity } from "./types.js";
6
7
  import { parseYaml } from "./yaml.js";
@@ -19,6 +20,7 @@ export {
19
20
  MAX_NESTING_DEPTH,
20
21
  applyEditOps,
21
22
  applyEditOpsWithChanges,
23
+ createJsonLocator,
22
24
  createLineMap,
23
25
  detectFormat,
24
26
  findExcessiveNesting,
@@ -0,0 +1,19 @@
1
+ import { type LineMap } from './lines.js';
2
+ import type { ILocation, JsonPath } from './types.js';
3
+ /**
4
+ * Resolves JSON paths to source ranges by scanning `source` on demand. The
5
+ * source is expected to be valid JSON; text that is not still gets an answer
6
+ * (and never a hang or a throw), just not a meaningful one.
7
+ *
8
+ * The eager alternative builds a node for every value in the document before
9
+ * the first lookup, which on a 25 MB spec costs several times the
10
+ * `JSON.parse` that produced the data. A lint run only asks for the handful
11
+ * of paths that carry findings, so this indexes just the containers a lookup
12
+ * passes through, once each: a container's members are listed the first time
13
+ * any lookup enters it, and every later lookup through it is a map read.
14
+ *
15
+ * Ranges are the ones a `jsonc-parser` tree gives for the same path: a
16
+ * string's range includes its quotes, a container's its brackets, and among
17
+ * duplicate keys the first one is found.
18
+ */
19
+ export declare const createJsonLocator: (source: string, lineMap?: LineMap) => ((path: JsonPath, closest?: boolean) => ILocation | undefined);
@@ -0,0 +1,151 @@
1
+ import { createLineMap } from "./lines.js";
2
+ const ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
3
+ const QUOTE = 34;
4
+ const BACKSLASH = 92;
5
+ const OPEN_BRACE = 123;
6
+ const CLOSE_BRACE = 125;
7
+ const OPEN_BRACKET = 91;
8
+ const CLOSE_BRACKET = 93;
9
+ const COMMA = 44;
10
+ const isWhitespace = (code) => code === 32 || code === 10 || code === 13 || code === 9;
11
+ const stringEnd = (source, start) => {
12
+ let quote = source.indexOf('"', start + 1);
13
+ while (quote !== -1) {
14
+ let slashes = 0;
15
+ for (let i = quote - 1; source.charCodeAt(i) === BACKSLASH; i--)
16
+ slashes++;
17
+ if ((slashes & 1) === 0)
18
+ return quote + 1;
19
+ quote = source.indexOf('"', quote + 1);
20
+ }
21
+ return -1;
22
+ };
23
+ const decodeKey = (literal) => {
24
+ const raw = literal.slice(1, -1);
25
+ if (!raw.includes("\\"))
26
+ return raw;
27
+ try {
28
+ return JSON.parse(literal);
29
+ } catch {
30
+ return raw;
31
+ }
32
+ };
33
+ const createJsonLocator = (source, lineMap) => {
34
+ const length = source.length;
35
+ const containers = /* @__PURE__ */ new Map();
36
+ let lines = lineMap;
37
+ const skipWhitespace = (offset) => {
38
+ let i = offset;
39
+ while (i < length && isWhitespace(source.charCodeAt(i)))
40
+ i++;
41
+ return i;
42
+ };
43
+ const valueEnd = (start) => {
44
+ const first = source.charCodeAt(start);
45
+ if (first === QUOTE)
46
+ return stringEnd(source, start);
47
+ if (first === OPEN_BRACE || first === OPEN_BRACKET) {
48
+ let depth = 0;
49
+ for (let i2 = start; i2 < length; i2++) {
50
+ const code = source.charCodeAt(i2);
51
+ if (code === QUOTE) {
52
+ const close = stringEnd(source, i2);
53
+ if (close === -1)
54
+ return length;
55
+ i2 = close - 1;
56
+ } else if (code === OPEN_BRACE || code === OPEN_BRACKET)
57
+ depth++;
58
+ else if (code === CLOSE_BRACE || code === CLOSE_BRACKET) {
59
+ depth--;
60
+ if (depth === 0)
61
+ return i2 + 1;
62
+ }
63
+ }
64
+ return length;
65
+ }
66
+ let i = start;
67
+ while (i < length) {
68
+ const code = source.charCodeAt(i);
69
+ if (code === COMMA || code === CLOSE_BRACE || code === CLOSE_BRACKET || isWhitespace(code))
70
+ break;
71
+ i++;
72
+ }
73
+ return i;
74
+ };
75
+ const index = (start) => {
76
+ const cached = containers.get(start);
77
+ if (cached !== void 0)
78
+ return cached;
79
+ const isObject = source.charCodeAt(start) === OPEN_BRACE;
80
+ const close = isObject ? CLOSE_BRACE : CLOSE_BRACKET;
81
+ const members = /* @__PURE__ */ new Map();
82
+ const starts = [];
83
+ const ends = [];
84
+ let i = skipWhitespace(start + 1);
85
+ while (i < length && source.charCodeAt(i) !== close) {
86
+ if (isObject) {
87
+ const keyEnd = source.charCodeAt(i) === QUOTE ? stringEnd(source, i) : -1;
88
+ if (keyEnd === -1)
89
+ break;
90
+ const key = decodeKey(source.slice(i, keyEnd));
91
+ if (!members.has(key))
92
+ members.set(key, starts.length);
93
+ i = skipWhitespace(skipWhitespace(keyEnd) + 1);
94
+ }
95
+ const end = valueEnd(i);
96
+ if (end <= i)
97
+ break;
98
+ starts.push(i);
99
+ ends.push(end);
100
+ i = skipWhitespace(end);
101
+ if (source.charCodeAt(i) === COMMA)
102
+ i = skipWhitespace(i + 1);
103
+ }
104
+ const container = isObject ? { kind: "object", members, starts, ends } : { kind: "array", starts, ends };
105
+ containers.set(start, container);
106
+ return container;
107
+ };
108
+ const toLocation = (start, end) => {
109
+ if (lines === void 0)
110
+ lines = createLineMap(source);
111
+ return { range: { start: lines.positionAt(start), end: lines.positionAt(end) } };
112
+ };
113
+ let rootStart = -1;
114
+ let rootEnd = -1;
115
+ return (path, closest = false) => {
116
+ if (rootStart === -1) {
117
+ rootStart = skipWhitespace(0);
118
+ rootEnd = length;
119
+ while (rootEnd > rootStart && isWhitespace(source.charCodeAt(rootEnd - 1)))
120
+ rootEnd--;
121
+ }
122
+ if (rootStart >= length)
123
+ return void 0;
124
+ let start = rootStart;
125
+ let end = rootEnd;
126
+ for (const segment of path) {
127
+ const first = source.charCodeAt(start);
128
+ let member = -1;
129
+ if (first === OPEN_BRACE) {
130
+ const container2 = index(start);
131
+ member = container2.members.get(String(segment)) ?? -1;
132
+ } else if (first === OPEN_BRACKET) {
133
+ const text = String(segment);
134
+ if (ARRAY_INDEX.test(text)) {
135
+ const position = Number(text);
136
+ if (position < index(start).starts.length)
137
+ member = position;
138
+ }
139
+ }
140
+ if (member === -1)
141
+ return closest ? toLocation(start, end) : void 0;
142
+ const container = index(start);
143
+ start = container.starts[member];
144
+ end = container.ends[member];
145
+ }
146
+ return toLocation(start, end);
147
+ };
148
+ };
149
+ export {
150
+ createJsonLocator
151
+ };
@@ -1,3 +1,10 @@
1
- import { type IParseResult } from './types.js';
1
+ import { type Node } from 'jsonc-parser';
2
+ import { type LineMap } from './lines.js';
3
+ import { type ILocation, type IParseResult, type JsonPath } from './types.js';
2
4
  /** Parses strict JSON into data plus a source map, reporting JSON-specific syntax errors. */
3
5
  export declare const parseJson: <T = unknown>(source: string) => IParseResult<T>;
6
+ /**
7
+ * Resolves paths against a `jsonc-parser` tree. When `closest` is set and the
8
+ * path leaves the document, the deepest node it reached is returned.
9
+ */
10
+ export declare const createTreeLocator: (root: Node, lineMap: LineMap) => (path: JsonPath, closest?: boolean) => ILocation | undefined;
@@ -1,12 +1,12 @@
1
1
  import { getNodeValue, parseTree, printParseErrorCode } from "jsonc-parser";
2
2
  import { findExcessiveNesting, MAX_NESTING_DEPTH } from "./depth.js";
3
+ import { createJsonLocator } from "./json-locator.js";
3
4
  import { createLineMap } from "./lines.js";
4
5
  import { DiagnosticSeverity } from "./types.js";
5
6
  const parseJson = (source) => {
6
- const lineMap = createLineMap(source);
7
7
  const excessive = findExcessiveNesting(source);
8
8
  if (excessive !== -1) {
9
- const position = lineMap.positionAt(excessive);
9
+ const position = createLineMap(source).positionAt(excessive);
10
10
  return {
11
11
  data: void 0,
12
12
  diagnostics: [
@@ -19,6 +19,10 @@ const parseJson = (source) => {
19
19
  getLocationForJsonPath: () => void 0
20
20
  };
21
21
  }
22
+ const fast = parseValidJson(source);
23
+ if (fast !== void 0)
24
+ return fast;
25
+ const lineMap = createLineMap(source);
22
26
  const errors = [];
23
27
  const root = parseTree(source, errors, { allowTrailingComma: false, disallowComments: true });
24
28
  const data = root ? getNodeValue(root) : void 0;
@@ -30,27 +34,35 @@ const parseJson = (source) => {
30
34
  end: lineMap.positionAt(err.offset + err.length)
31
35
  }
32
36
  }));
33
- const getLocationForJsonPath = (path, closest = false) => {
34
- if (!root)
35
- return void 0;
36
- const p = path.slice();
37
- while (true) {
38
- const node = findNode(root, p);
39
- if (node) {
40
- return {
41
- range: {
42
- start: lineMap.positionAt(node.offset),
43
- end: lineMap.positionAt(node.offset + node.length)
44
- }
45
- };
46
- }
47
- if (!closest || p.length === 0)
48
- return void 0;
49
- p.pop();
50
- }
51
- };
37
+ const getLocationForJsonPath = root ? createTreeLocator(root, lineMap) : () => void 0;
52
38
  return { data, diagnostics, getLocationForJsonPath };
53
39
  };
40
+ const parseValidJson = (source) => {
41
+ let data;
42
+ try {
43
+ data = JSON.parse(source);
44
+ } catch {
45
+ return void 0;
46
+ }
47
+ return { data, diagnostics: [], getLocationForJsonPath: createJsonLocator(source) };
48
+ };
49
+ const createTreeLocator = (root, lineMap) => (path, closest = false) => {
50
+ const p = path.slice();
51
+ while (true) {
52
+ const node = findNode(root, p);
53
+ if (node) {
54
+ return {
55
+ range: {
56
+ start: lineMap.positionAt(node.offset),
57
+ end: lineMap.positionAt(node.offset + node.length)
58
+ }
59
+ };
60
+ }
61
+ if (!closest || p.length === 0)
62
+ return void 0;
63
+ p.pop();
64
+ }
65
+ };
54
66
  const ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
55
67
  const findNode = (root, path) => {
56
68
  let node = root;
@@ -71,5 +83,6 @@ const findNode = (root, path) => {
71
83
  return node;
72
84
  };
73
85
  export {
86
+ createTreeLocator,
74
87
  parseJson
75
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/lint",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "A fast, format-agnostic JSON/YAML style-guide linter with JSON Schema and custom rules.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -46,6 +46,7 @@
46
46
  "test": "NODE_ENV=production vitest run --root ../.. packages/lint/",
47
47
  "prebench": "bun run build && bun run --filter='@amritk/runtime-validators' --filter='@amritk/yaml' --filter='@amritk/resolve-refs' build",
48
48
  "bench": "bun run ./bench/run.ts",
49
+ "bench:large": "bun --conditions development ./bench/large.ts",
49
50
  "prebench:node": "bun run build && bun run --filter='@amritk/runtime-validators' --filter='@amritk/yaml' --filter='@amritk/resolve-refs' build",
50
51
  "bench:node": "node ./bench/run.ts"
51
52
  },