@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Document, type IDocumentOptions } from './document.js';
|
|
2
|
+
import type { LintPlugin } from './plugin.js';
|
|
3
|
+
import type { Ruleset } from './ruleset.js';
|
|
4
|
+
import { type IDiagnostic, type ISourceSet } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* How a resolved (`$ref`-dereferenced) tree is produced from a parsed document.
|
|
7
|
+
* The engine stays free of any `$ref` resolver: a caller may inject one (for
|
|
8
|
+
* example wrapping `@amritk/resolve-refs`). Returning no `sources` means
|
|
9
|
+
* findings map back to the root document only.
|
|
10
|
+
*/
|
|
11
|
+
export type LintResolver = (document: Document, context: {
|
|
12
|
+
input: string;
|
|
13
|
+
}) => {
|
|
14
|
+
resolved: unknown;
|
|
15
|
+
sources?: ISourceSet;
|
|
16
|
+
} | Promise<{
|
|
17
|
+
resolved: unknown;
|
|
18
|
+
sources?: ISourceSet;
|
|
19
|
+
}>;
|
|
20
|
+
/** Options for {@link lint} / {@link lintWithResult}. */
|
|
21
|
+
export type LintOptions = IDocumentOptions & {
|
|
22
|
+
/** A normalized ruleset to evaluate (built via {@link createRuleset}). */
|
|
23
|
+
ruleset: Ruleset;
|
|
24
|
+
/**
|
|
25
|
+
* Produces the resolved tree for rules with `resolved: true`. When omitted the
|
|
26
|
+
* raw parsed data is used as-is (no `$ref` dereferencing).
|
|
27
|
+
*/
|
|
28
|
+
resolve?: LintResolver;
|
|
29
|
+
/**
|
|
30
|
+
* Checked against the parsed data before any resolution: when it returns true
|
|
31
|
+
* the document is skipped and produces no findings. Presets use this to skip
|
|
32
|
+
* documents of an unrecognized format without paying for `$ref` resolution.
|
|
33
|
+
*/
|
|
34
|
+
skip?: (data: unknown) => boolean;
|
|
35
|
+
/** Plugins run after the rule pass via {@link runPlugins} (e.g. auto-fix). */
|
|
36
|
+
plugins?: LintPlugin[];
|
|
37
|
+
};
|
|
38
|
+
/** The full result of a {@link lintWithResult} run: findings plus plugin output. */
|
|
39
|
+
export type LintResult = {
|
|
40
|
+
diagnostics: IDiagnostic[];
|
|
41
|
+
/** A rewritten document, when a plugin (e.g. auto-fix) produced one. */
|
|
42
|
+
output?: string;
|
|
43
|
+
/** Per-plugin structured output, keyed by plugin name. */
|
|
44
|
+
pluginData: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Lints `input` against a normalized `ruleset`, returning the full
|
|
48
|
+
* {@link LintResult} (findings plus anything the configured `plugins` produced).
|
|
49
|
+
* This is the format-agnostic pipeline — parse with source maps → resolve (via
|
|
50
|
+
* the injected {@link LintResolver}) → run the ruleset → run plugins. A higher
|
|
51
|
+
* level entry point (such as `@amritk/lint`) supplies the ruleset, and
|
|
52
|
+
* optionally a resolver and format-skip predicate; {@link lint} is a thin
|
|
53
|
+
* wrapper returning just the diagnostics.
|
|
54
|
+
*/
|
|
55
|
+
export declare const lintWithResult: (input: string, options: LintOptions) => Promise<LintResult>;
|
|
56
|
+
/** Lints `input` against a normalized `ruleset` and returns just the findings. */
|
|
57
|
+
export declare const lint: (input: string, options: LintOptions) => Promise<IDiagnostic[]>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { detectFormat } from '../parsers/index.js';
|
|
2
|
+
import { createDocument } from './document.js';
|
|
3
|
+
import { runPlugins } from './plugin.js';
|
|
4
|
+
import { createLinter } from './runner.js';
|
|
5
|
+
import { DiagnosticSeverity } from './types.js';
|
|
6
|
+
const toParserSeverity = (value) => {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof value === 'number')
|
|
10
|
+
return value;
|
|
11
|
+
if (value === 'off')
|
|
12
|
+
return 'off';
|
|
13
|
+
const names = {
|
|
14
|
+
error: DiagnosticSeverity.Error,
|
|
15
|
+
warn: DiagnosticSeverity.Warning,
|
|
16
|
+
info: DiagnosticSeverity.Information,
|
|
17
|
+
hint: DiagnosticSeverity.Hint,
|
|
18
|
+
};
|
|
19
|
+
return names[value];
|
|
20
|
+
};
|
|
21
|
+
const byPosition = (a, b) => a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character;
|
|
22
|
+
/**
|
|
23
|
+
* Lints `input` against a normalized `ruleset`, returning the full
|
|
24
|
+
* {@link LintResult} (findings plus anything the configured `plugins` produced).
|
|
25
|
+
* This is the format-agnostic pipeline — parse with source maps → resolve (via
|
|
26
|
+
* the injected {@link LintResolver}) → run the ruleset → run plugins. A higher
|
|
27
|
+
* level entry point (such as `@amritk/lint`) supplies the ruleset, and
|
|
28
|
+
* optionally a resolver and format-skip predicate; {@link lint} is a thin
|
|
29
|
+
* wrapper returning just the diagnostics.
|
|
30
|
+
*/
|
|
31
|
+
export const lintWithResult = async (input, options) => {
|
|
32
|
+
const { ruleset, resolve, skip, plugins, ...documentOptions } = options;
|
|
33
|
+
const duplicateKeys = toParserSeverity(ruleset.parserOptions?.duplicateKeys);
|
|
34
|
+
const incompatibleValues = toParserSeverity(ruleset.parserOptions?.incompatibleValues);
|
|
35
|
+
const document = createDocument(input, {
|
|
36
|
+
...documentOptions,
|
|
37
|
+
...(duplicateKeys !== undefined ? { duplicateKeys } : {}),
|
|
38
|
+
...(incompatibleValues !== undefined ? { incompatibleValues } : {}),
|
|
39
|
+
});
|
|
40
|
+
// Skip unrecognized-format documents before paying for resolution or the run.
|
|
41
|
+
if (skip?.(document.data))
|
|
42
|
+
return { diagnostics: [], pluginData: {} };
|
|
43
|
+
let resolved = document.data;
|
|
44
|
+
let sources;
|
|
45
|
+
if (resolve) {
|
|
46
|
+
const result = await resolve(document, { input });
|
|
47
|
+
resolved = result.resolved;
|
|
48
|
+
sources = result.sources;
|
|
49
|
+
}
|
|
50
|
+
const ruleResults = createLinter(ruleset).run(document, { resolved, ...(sources ? { sources } : {}) });
|
|
51
|
+
const parserResults = document.diagnostics.map((diagnostic) => {
|
|
52
|
+
const result = {
|
|
53
|
+
code: 'parser',
|
|
54
|
+
message: diagnostic.message,
|
|
55
|
+
path: diagnostic.path ?? [],
|
|
56
|
+
severity: diagnostic.severity,
|
|
57
|
+
range: diagnostic.range,
|
|
58
|
+
};
|
|
59
|
+
if (document.source !== undefined)
|
|
60
|
+
result.source = document.source;
|
|
61
|
+
return result;
|
|
62
|
+
});
|
|
63
|
+
const diagnostics = [...parserResults, ...ruleResults].sort(byPosition);
|
|
64
|
+
if (!plugins || plugins.length === 0)
|
|
65
|
+
return { diagnostics, pluginData: {} };
|
|
66
|
+
const format = documentOptions.format ?? detectFormat(input);
|
|
67
|
+
const run = runPlugins(plugins, diagnostics, {
|
|
68
|
+
input,
|
|
69
|
+
format,
|
|
70
|
+
document,
|
|
71
|
+
resolved,
|
|
72
|
+
ruleset,
|
|
73
|
+
...(documentOptions.source !== undefined ? { source: documentOptions.source } : {}),
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
diagnostics: run.diagnostics,
|
|
77
|
+
pluginData: run.data,
|
|
78
|
+
...(run.output !== undefined ? { output: run.output } : {}),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/** Lints `input` against a normalized `ruleset` and returns just the findings. */
|
|
82
|
+
export const lint = async (input, options) => (await lintWithResult(input, options)).diagnostics;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ParserFormat } from '../parsers/index.js';
|
|
2
|
+
import type { Document } from './document.js';
|
|
3
|
+
import type { Ruleset } from './ruleset.js';
|
|
4
|
+
import type { IDiagnostic } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Everything a plugin needs about the document it is post-processing: the raw
|
|
7
|
+
* input text and its format (so a plugin can produce text edits), the parsed
|
|
8
|
+
* {@link Document} with its source map, the dereferenced tree, and the ruleset
|
|
9
|
+
* that produced the findings.
|
|
10
|
+
*/
|
|
11
|
+
export type LintPluginContext = {
|
|
12
|
+
source?: string | undefined;
|
|
13
|
+
/** The original, unparsed document text. */
|
|
14
|
+
input: string;
|
|
15
|
+
/** Which concrete parser the document was routed to (drives text-edit serialization). */
|
|
16
|
+
format: ParserFormat;
|
|
17
|
+
document: Document;
|
|
18
|
+
/** The dereferenced document data, as passed to the runner. */
|
|
19
|
+
resolved: unknown;
|
|
20
|
+
ruleset: Ruleset;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* What a plugin's `afterLint` hook may return. Every field is optional: a plugin
|
|
24
|
+
* that only observes returns nothing. A plugin that rewrites the document (e.g.
|
|
25
|
+
* auto-fix) returns `output`; one that resolves or adds findings returns
|
|
26
|
+
* `diagnostics`; structured results go in `data` (keyed by plugin name).
|
|
27
|
+
*/
|
|
28
|
+
export type LintPluginResult = {
|
|
29
|
+
/** A rewritten version of the document text (e.g. with fixes applied). */
|
|
30
|
+
output?: string;
|
|
31
|
+
/** Diagnostics that replace the ones passed in (e.g. with fixed ones removed). */
|
|
32
|
+
diagnostics?: IDiagnostic[];
|
|
33
|
+
/** Arbitrary structured output, surfaced under the plugin's name. */
|
|
34
|
+
data?: unknown;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A Linter plugin. The lint pipeline invokes registered plugins after the rule
|
|
38
|
+
* run via {@link runPlugins}. The hook is intentionally small — a single
|
|
39
|
+
* post-lint pass — so the core engine stays unaware of any specific plugin (the
|
|
40
|
+
* auto-fix subsystem, for instance, is just a plugin that returns `output`).
|
|
41
|
+
*/
|
|
42
|
+
export type LintPlugin = {
|
|
43
|
+
name: string;
|
|
44
|
+
afterLint?: (diagnostics: IDiagnostic[], context: LintPluginContext) => LintPluginResult | undefined;
|
|
45
|
+
};
|
|
46
|
+
/** The combined result of running every plugin over a document's findings. */
|
|
47
|
+
export type PluginRunResult = {
|
|
48
|
+
diagnostics: IDiagnostic[];
|
|
49
|
+
/** The last `output` any plugin produced, if any rewrote the document. */
|
|
50
|
+
output?: string;
|
|
51
|
+
/** Per-plugin structured `data`, keyed by plugin name. */
|
|
52
|
+
data: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Runs `plugins` in order over `diagnostics`. Each plugin sees the (possibly
|
|
56
|
+
* already transformed) diagnostics from the previous one; when a plugin returns
|
|
57
|
+
* `output`, later plugins and the context see that rewritten text as `input`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const runPlugins: (plugins: readonly LintPlugin[], diagnostics: IDiagnostic[], context: LintPluginContext) => PluginRunResult;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `plugins` in order over `diagnostics`. Each plugin sees the (possibly
|
|
3
|
+
* already transformed) diagnostics from the previous one; when a plugin returns
|
|
4
|
+
* `output`, later plugins and the context see that rewritten text as `input`.
|
|
5
|
+
*/
|
|
6
|
+
export const runPlugins = (plugins, diagnostics, context) => {
|
|
7
|
+
let currentDiagnostics = diagnostics;
|
|
8
|
+
let currentInput = context.input;
|
|
9
|
+
let output;
|
|
10
|
+
const data = {};
|
|
11
|
+
for (const plugin of plugins) {
|
|
12
|
+
const result = plugin.afterLint?.(currentDiagnostics, { ...context, input: currentInput });
|
|
13
|
+
if (!result)
|
|
14
|
+
continue;
|
|
15
|
+
if (result.diagnostics)
|
|
16
|
+
currentDiagnostics = result.diagnostics;
|
|
17
|
+
if (result.output !== undefined) {
|
|
18
|
+
output = result.output;
|
|
19
|
+
currentInput = result.output;
|
|
20
|
+
}
|
|
21
|
+
if (result.data !== undefined)
|
|
22
|
+
data[plugin.name] = result.data;
|
|
23
|
+
}
|
|
24
|
+
return { diagnostics: currentDiagnostics, data, ...(output !== undefined ? { output } : {}) };
|
|
25
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IDocumentRegistry, IOriginMap, ISourceOrigin, JsonPath } from './types.js';
|
|
2
|
+
/** Parses an internal JSON pointer (`#/a/b`) into a path; returns undefined for external refs. */
|
|
3
|
+
export declare const pointerToPath: (pointer: string) => JsonPath | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Translates a path into the *resolved* (dereferenced) document back into the
|
|
6
|
+
* equivalent path in the *original* source document, following internal `$ref`s.
|
|
7
|
+
* This lets findings produced against the resolved tree resolve to the exact
|
|
8
|
+
* line:column of the original `$ref` target. External refs stop the walk at the
|
|
9
|
+
* `$ref` site.
|
|
10
|
+
*/
|
|
11
|
+
export declare const resolveSourcePath: (root: unknown, path: JsonPath) => JsonPath;
|
|
12
|
+
/**
|
|
13
|
+
* Cross-document generalization of {@link resolveSourcePath}: translates a path
|
|
14
|
+
* in the *resolved* (dereferenced) tree back to the document it actually came
|
|
15
|
+
* from and the path within it, following both internal (`#/...`) and external
|
|
16
|
+
* (other-file / remote) `$ref`s through the {@link IDocumentRegistry}. This is
|
|
17
|
+
* what lets a finding on a node inlined from `./pet.yaml` report `pet.yaml`'s own
|
|
18
|
+
* line:column instead of the `$ref` site in the root.
|
|
19
|
+
*
|
|
20
|
+
* The walk re-derives the resolver's traversal over the *unresolved* documents,
|
|
21
|
+
* so a `$ref` whose target file is not in the registry (e.g. it failed to load)
|
|
22
|
+
* stops the walk at the last known location rather than guessing. Prefer
|
|
23
|
+
* {@link resolveSourceOriginFromMap} when the resolver supplies an origin map —
|
|
24
|
+
* it avoids re-deriving this traversal.
|
|
25
|
+
*/
|
|
26
|
+
export declare const resolveSourceOrigin: (registry: IDocumentRegistry, path: JsonPath) => ISourceOrigin;
|
|
27
|
+
/**
|
|
28
|
+
* Single-walk equivalent of {@link resolveSourceOrigin} that uses the resolver's
|
|
29
|
+
* per-node origin map instead of re-walking the unresolved documents. We descend
|
|
30
|
+
* the *resolved* tree along `path`; whenever we step onto a node the resolver
|
|
31
|
+
* stamped (it was inlined from a `$ref`), we re-base onto that node's origin
|
|
32
|
+
* document and in-file path, then keep appending the remaining segments. Nodes
|
|
33
|
+
* straight from the root carry no stamp, so they stay attributed to the root.
|
|
34
|
+
*/
|
|
35
|
+
export declare const resolveSourceOriginFromMap: (resolved: unknown, origins: IOriginMap, rootLocation: string, path: JsonPath) => ISourceOrigin;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
2
|
+
const isContainer = (value) => typeof value === 'object' && value !== null;
|
|
3
|
+
const decodeSegment = (segment) => {
|
|
4
|
+
const decoded = segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
5
|
+
return /^\d+$/.test(decoded) ? Number(decoded) : decoded;
|
|
6
|
+
};
|
|
7
|
+
/** Parses an internal JSON pointer (`#/a/b`) into a path; returns undefined for external refs. */
|
|
8
|
+
export const pointerToPath = (pointer) => {
|
|
9
|
+
if (pointer === '#')
|
|
10
|
+
return [];
|
|
11
|
+
if (!pointer.startsWith('#/'))
|
|
12
|
+
return undefined;
|
|
13
|
+
return pointer.slice(2).split('/').map(decodeSegment);
|
|
14
|
+
};
|
|
15
|
+
const isRemote = (location) => /^https?:\/\//i.test(location);
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the location of `ref` (its file/URL part) relative to `base`, exactly
|
|
18
|
+
* as `@amritk/resolve-refs` does, so the keys we look up in the source set match
|
|
19
|
+
* the documents the resolver loaded.
|
|
20
|
+
*/
|
|
21
|
+
const joinLocation = (base, ref) => {
|
|
22
|
+
if (isRemote(ref))
|
|
23
|
+
return ref;
|
|
24
|
+
if (isRemote(base))
|
|
25
|
+
return new URL(ref, base).href;
|
|
26
|
+
return resolvePath(dirname(base), ref);
|
|
27
|
+
};
|
|
28
|
+
/** Splits a `$ref` into its document part and JSON-pointer fragment (without `#`). */
|
|
29
|
+
const splitRef = (ref) => {
|
|
30
|
+
const hashIndex = ref.indexOf('#');
|
|
31
|
+
return {
|
|
32
|
+
filePart: hashIndex === -1 ? ref : ref.slice(0, hashIndex),
|
|
33
|
+
fragment: hashIndex === -1 ? '' : ref.slice(hashIndex + 1),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Parses a JSON-pointer fragment (the part after `#`, e.g. `/a/b`) into a path,
|
|
38
|
+
* mirroring `getByPointer`'s decoding (`%XX` then `~1`/`~0`) so cross-file
|
|
39
|
+
* pointers resolve to the same nodes the resolver inlined.
|
|
40
|
+
*/
|
|
41
|
+
const fragmentToPath = (fragment) => {
|
|
42
|
+
if (fragment === '' || fragment === '/')
|
|
43
|
+
return [];
|
|
44
|
+
return fragment
|
|
45
|
+
.replace(/^\//, '')
|
|
46
|
+
.split('/')
|
|
47
|
+
.map((segment) => {
|
|
48
|
+
let decoded = segment;
|
|
49
|
+
try {
|
|
50
|
+
decoded = decodeURIComponent(segment);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// leave invalid percent-escapes as-is
|
|
54
|
+
}
|
|
55
|
+
decoded = decoded.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
56
|
+
return /^\d+$/.test(decoded) ? Number(decoded) : decoded;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const getAtPath = (root, path) => {
|
|
60
|
+
let node = root;
|
|
61
|
+
for (const segment of path) {
|
|
62
|
+
if (!isContainer(node))
|
|
63
|
+
return undefined;
|
|
64
|
+
node = node[segment];
|
|
65
|
+
}
|
|
66
|
+
return node;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Translates a path into the *resolved* (dereferenced) document back into the
|
|
70
|
+
* equivalent path in the *original* source document, following internal `$ref`s.
|
|
71
|
+
* This lets findings produced against the resolved tree resolve to the exact
|
|
72
|
+
* line:column of the original `$ref` target. External refs stop the walk at the
|
|
73
|
+
* `$ref` site.
|
|
74
|
+
*/
|
|
75
|
+
export const resolveSourcePath = (root, path) => {
|
|
76
|
+
let originalPath = [];
|
|
77
|
+
let node = root;
|
|
78
|
+
const followRefs = () => {
|
|
79
|
+
let guard = 0;
|
|
80
|
+
while (isContainer(node) && !Array.isArray(node) && typeof node['$ref'] === 'string' && guard++ < 100) {
|
|
81
|
+
const target = pointerToPath(node['$ref']);
|
|
82
|
+
if (!target)
|
|
83
|
+
return;
|
|
84
|
+
originalPath = [...target];
|
|
85
|
+
node = getAtPath(root, target);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
followRefs();
|
|
89
|
+
for (const segment of path) {
|
|
90
|
+
if (!isContainer(node))
|
|
91
|
+
break;
|
|
92
|
+
originalPath.push(segment);
|
|
93
|
+
node = node[segment];
|
|
94
|
+
followRefs();
|
|
95
|
+
}
|
|
96
|
+
return originalPath;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Cross-document generalization of {@link resolveSourcePath}: translates a path
|
|
100
|
+
* in the *resolved* (dereferenced) tree back to the document it actually came
|
|
101
|
+
* from and the path within it, following both internal (`#/...`) and external
|
|
102
|
+
* (other-file / remote) `$ref`s through the {@link IDocumentRegistry}. This is
|
|
103
|
+
* what lets a finding on a node inlined from `./pet.yaml` report `pet.yaml`'s own
|
|
104
|
+
* line:column instead of the `$ref` site in the root.
|
|
105
|
+
*
|
|
106
|
+
* The walk re-derives the resolver's traversal over the *unresolved* documents,
|
|
107
|
+
* so a `$ref` whose target file is not in the registry (e.g. it failed to load)
|
|
108
|
+
* stops the walk at the last known location rather than guessing. Prefer
|
|
109
|
+
* {@link resolveSourceOriginFromMap} when the resolver supplies an origin map —
|
|
110
|
+
* it avoids re-deriving this traversal.
|
|
111
|
+
*/
|
|
112
|
+
export const resolveSourceOrigin = (registry, path) => {
|
|
113
|
+
let location = registry.rootLocation;
|
|
114
|
+
let root = registry.get(location)?.data;
|
|
115
|
+
let originalPath = [];
|
|
116
|
+
let node = root;
|
|
117
|
+
const followRefs = () => {
|
|
118
|
+
let guard = 0;
|
|
119
|
+
while (isContainer(node) && !Array.isArray(node) && typeof node['$ref'] === 'string' && guard++ < 1000) {
|
|
120
|
+
const { filePart, fragment } = splitRef(node['$ref']);
|
|
121
|
+
if (filePart !== '') {
|
|
122
|
+
const target = joinLocation(location, filePart);
|
|
123
|
+
const doc = registry.get(target);
|
|
124
|
+
if (!doc)
|
|
125
|
+
return;
|
|
126
|
+
location = target;
|
|
127
|
+
root = doc.data;
|
|
128
|
+
}
|
|
129
|
+
originalPath = fragmentToPath(fragment);
|
|
130
|
+
node = getAtPath(root, originalPath);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
followRefs();
|
|
134
|
+
for (const segment of path) {
|
|
135
|
+
if (!isContainer(node))
|
|
136
|
+
break;
|
|
137
|
+
originalPath.push(segment);
|
|
138
|
+
node = node[segment];
|
|
139
|
+
followRefs();
|
|
140
|
+
}
|
|
141
|
+
return { location, path: originalPath };
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Single-walk equivalent of {@link resolveSourceOrigin} that uses the resolver's
|
|
145
|
+
* per-node origin map instead of re-walking the unresolved documents. We descend
|
|
146
|
+
* the *resolved* tree along `path`; whenever we step onto a node the resolver
|
|
147
|
+
* stamped (it was inlined from a `$ref`), we re-base onto that node's origin
|
|
148
|
+
* document and in-file path, then keep appending the remaining segments. Nodes
|
|
149
|
+
* straight from the root carry no stamp, so they stay attributed to the root.
|
|
150
|
+
*/
|
|
151
|
+
export const resolveSourceOriginFromMap = (resolved, origins, rootLocation, path) => {
|
|
152
|
+
let location = rootLocation;
|
|
153
|
+
let inFilePath = [];
|
|
154
|
+
let node = resolved;
|
|
155
|
+
for (const segment of path) {
|
|
156
|
+
if (!isContainer(node))
|
|
157
|
+
break;
|
|
158
|
+
node = node[segment];
|
|
159
|
+
const stamp = isContainer(node) ? origins.get(node) : undefined;
|
|
160
|
+
if (stamp) {
|
|
161
|
+
location = stamp.location;
|
|
162
|
+
inFilePath = [...stamp.pointer];
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
inFilePath.push(segment);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { location, path: inFilePath };
|
|
169
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Format } from './formats.js';
|
|
2
|
+
import type { FunctionRegistry, IRulesetOverride, ResolvedRule, RulesetDefinition, RulesetFunction } from './types.js';
|
|
3
|
+
/** Controls which rules an `extends` target contributes: everything, only recommended, or none. */
|
|
4
|
+
export type ExtendModifier = 'all' | 'recommended' | 'off';
|
|
5
|
+
/** A ruleset resolved from an `extends` reference, plus the base directory its own extends resolve from. */
|
|
6
|
+
export type ResolvedExtend = {
|
|
7
|
+
definition: RulesetDefinition;
|
|
8
|
+
basePath: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a string `extends` target (a built-in name, file path, or npm package)
|
|
12
|
+
* to a ruleset definition. `basePath` is the directory the reference is relative to.
|
|
13
|
+
*/
|
|
14
|
+
export type ExtendResolver = (name: string, basePath: string) => ResolvedExtend;
|
|
15
|
+
/** An alias target: either a flat list of `given`s, or per-format `given`s with an optional description. */
|
|
16
|
+
export type AliasDefinition = string[] | {
|
|
17
|
+
description?: string;
|
|
18
|
+
targets: {
|
|
19
|
+
formats: string[];
|
|
20
|
+
given: string[];
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
/** Inputs {@link createRuleset} needs beyond the definition: functions, formats, and how to resolve `extends`. */
|
|
24
|
+
export type RulesetOptions = {
|
|
25
|
+
functions?: FunctionRegistry;
|
|
26
|
+
formats?: Record<string, Format>;
|
|
27
|
+
resolve?: ExtendResolver;
|
|
28
|
+
/** Directory that string `extends` references resolve relative to. Defaults to the cwd. */
|
|
29
|
+
basePath?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* A normalized ruleset: every `extends`, severity override, and modifier already
|
|
33
|
+
* applied, exposing the helpers the runner needs to evaluate it against a
|
|
34
|
+
* document. Produced by {@link createRuleset}.
|
|
35
|
+
*/
|
|
36
|
+
export type Ruleset = {
|
|
37
|
+
readonly rules: ResolvedRule[];
|
|
38
|
+
readonly functions: FunctionRegistry;
|
|
39
|
+
readonly formats: Record<string, Format>;
|
|
40
|
+
readonly aliases: Record<string, AliasDefinition>;
|
|
41
|
+
readonly overrides: IRulesetOverride[];
|
|
42
|
+
readonly parserOptions: RulesetDefinition['parserOptions'];
|
|
43
|
+
/** The subset of `rules` that are enabled. */
|
|
44
|
+
readonly enabledRules: ResolvedRule[];
|
|
45
|
+
getFunction(name: string): RulesetFunction | undefined;
|
|
46
|
+
/** Returns the effective rules for a document, applying matching overrides by file glob. */
|
|
47
|
+
rulesForSource(source: string | undefined): ResolvedRule[];
|
|
48
|
+
/** Resolves `#alias` references in `given` expressions for the document's formats. */
|
|
49
|
+
expandGiven(given: string[], formats: Set<string>): string[];
|
|
50
|
+
};
|
|
51
|
+
/** Normalizes a ruleset definition into a {@link Ruleset} ready to run. */
|
|
52
|
+
export declare const createRuleset: (definition: RulesetDefinition, options?: RulesetOptions) => Ruleset;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from '../parsers/index.js';
|
|
2
|
+
import { matchesGlob } from './glob.js';
|
|
3
|
+
const SEVERITY_NAMES = {
|
|
4
|
+
error: DiagnosticSeverity.Error,
|
|
5
|
+
warn: DiagnosticSeverity.Warning,
|
|
6
|
+
info: DiagnosticSeverity.Information,
|
|
7
|
+
hint: DiagnosticSeverity.Hint,
|
|
8
|
+
off: 'off',
|
|
9
|
+
};
|
|
10
|
+
const parseSeverity = (value) => {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return { severity: DiagnosticSeverity.Warning, enabled: true };
|
|
13
|
+
if (typeof value === 'number')
|
|
14
|
+
return { severity: value, enabled: true };
|
|
15
|
+
const mapped = SEVERITY_NAMES[value];
|
|
16
|
+
if (mapped === 'off')
|
|
17
|
+
return { severity: DiagnosticSeverity.Warning, enabled: false };
|
|
18
|
+
return { severity: mapped, enabled: true };
|
|
19
|
+
};
|
|
20
|
+
const toArray = (value) => (Array.isArray(value) ? value : [value]);
|
|
21
|
+
const normalizeRule = (name, def, modifier) => {
|
|
22
|
+
const { severity, enabled } = parseSeverity(def.severity);
|
|
23
|
+
const recommended = def.recommended ?? true;
|
|
24
|
+
// Under the 'recommended' modifier only recommended rules are on; 'all' turns
|
|
25
|
+
// everything on; 'off' turns everything off.
|
|
26
|
+
const modifierEnabled = modifier === 'off' ? false : modifier === 'recommended' ? recommended : true;
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
description: def.description,
|
|
30
|
+
message: def.message,
|
|
31
|
+
severity,
|
|
32
|
+
enabled: enabled && modifierEnabled,
|
|
33
|
+
given: toArray(def.given),
|
|
34
|
+
then: toArray(def.then),
|
|
35
|
+
formats: def.formats ? new Set(def.formats) : undefined,
|
|
36
|
+
recommended,
|
|
37
|
+
resolved: def.resolved ?? true,
|
|
38
|
+
documentationUrl: def.documentationUrl,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const applyEntry = (rules, name, entry) => {
|
|
42
|
+
if (typeof entry === 'boolean') {
|
|
43
|
+
const existing = rules.get(name);
|
|
44
|
+
if (existing)
|
|
45
|
+
existing.enabled = entry;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (typeof entry === 'string') {
|
|
49
|
+
const existing = rules.get(name);
|
|
50
|
+
const { severity, enabled } = parseSeverity(entry);
|
|
51
|
+
if (existing) {
|
|
52
|
+
existing.severity = severity;
|
|
53
|
+
existing.enabled = enabled;
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Full definition: define or replace (always enabled per its own severity).
|
|
58
|
+
rules.set(name, normalizeRule(name, entry, 'all'));
|
|
59
|
+
};
|
|
60
|
+
const collectExtends = (extendsValue, defaultModifier, resolve, into, basePath) => {
|
|
61
|
+
const entries = Array.isArray(extendsValue) ? extendsValue : [extendsValue];
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
let target;
|
|
64
|
+
let modifier = defaultModifier;
|
|
65
|
+
if (Array.isArray(entry)) {
|
|
66
|
+
target = entry[0];
|
|
67
|
+
modifier = entry[1];
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
target = entry;
|
|
71
|
+
}
|
|
72
|
+
if (typeof target === 'string') {
|
|
73
|
+
// A resolved file/package brings its own base directory for any nested extends.
|
|
74
|
+
const resolved = resolveNamed(target, resolve, basePath);
|
|
75
|
+
mergeInto(resolved.definition, modifier, resolve, into, resolved.basePath);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
mergeInto(target, modifier, resolve, into, basePath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const resolveNamed = (name, resolve, basePath) => {
|
|
83
|
+
if (!resolve)
|
|
84
|
+
throw new Error(`Cannot resolve extended ruleset "${name}": no resolver provided`);
|
|
85
|
+
return resolve(name, basePath);
|
|
86
|
+
};
|
|
87
|
+
const mergeInto = (definition, modifier, resolve, into, basePath) => {
|
|
88
|
+
if (definition.extends) {
|
|
89
|
+
collectExtends(definition.extends, 'recommended', resolve, into, basePath);
|
|
90
|
+
}
|
|
91
|
+
if (definition.rules) {
|
|
92
|
+
for (const [name, entry] of Object.entries(definition.rules)) {
|
|
93
|
+
if (typeof entry === 'object') {
|
|
94
|
+
into.set(name, normalizeRule(name, entry, modifier));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
applyEntry(into, name, entry);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const cloneRule = (rule) => ({
|
|
103
|
+
...rule,
|
|
104
|
+
given: [...rule.given],
|
|
105
|
+
then: [...rule.then],
|
|
106
|
+
formats: rule.formats ? new Set(rule.formats) : undefined,
|
|
107
|
+
});
|
|
108
|
+
/** Normalizes a ruleset definition into a {@link Ruleset} ready to run. */
|
|
109
|
+
export const createRuleset = (definition, options = {}) => {
|
|
110
|
+
const functions = options.functions ?? {};
|
|
111
|
+
const formats = options.formats ?? {};
|
|
112
|
+
const aliases = (definition.aliases ?? {});
|
|
113
|
+
const overrides = definition.overrides ?? [];
|
|
114
|
+
const map = new Map();
|
|
115
|
+
mergeInto(definition, 'all', options.resolve, map, options.basePath ?? process.cwd());
|
|
116
|
+
const rules = [...map.values()];
|
|
117
|
+
const resolveAlias = (name, documentFormats) => {
|
|
118
|
+
const alias = aliases[name];
|
|
119
|
+
if (!alias)
|
|
120
|
+
return [];
|
|
121
|
+
if (Array.isArray(alias))
|
|
122
|
+
return alias;
|
|
123
|
+
for (const target of alias.targets) {
|
|
124
|
+
if (target.formats.some((format) => documentFormats.has(format)))
|
|
125
|
+
return target.given;
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
rules,
|
|
131
|
+
functions,
|
|
132
|
+
formats,
|
|
133
|
+
aliases,
|
|
134
|
+
overrides,
|
|
135
|
+
parserOptions: definition.parserOptions,
|
|
136
|
+
enabledRules: rules.filter((rule) => rule.enabled),
|
|
137
|
+
getFunction: (name) => functions[name],
|
|
138
|
+
rulesForSource: (source) => {
|
|
139
|
+
if (!source || overrides.length === 0)
|
|
140
|
+
return rules;
|
|
141
|
+
const scoped = new Map(rules.map((rule) => [rule.name, cloneRule(rule)]));
|
|
142
|
+
for (const override of overrides) {
|
|
143
|
+
// Pointer-scoped entries (`file#/path`) are applied per-finding, not here.
|
|
144
|
+
const patterns = override.files.filter((file) => !file.includes('#'));
|
|
145
|
+
if (patterns.length === 0 || !matchesGlob(source, patterns))
|
|
146
|
+
continue;
|
|
147
|
+
if (override.rules) {
|
|
148
|
+
for (const [name, entry] of Object.entries(override.rules))
|
|
149
|
+
applyEntry(scoped, name, entry);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...scoped.values()];
|
|
153
|
+
},
|
|
154
|
+
expandGiven: (given, documentFormats) => {
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const expression of given) {
|
|
157
|
+
const match = /^#([A-Za-z0-9_-]+)(.*)$/.exec(expression);
|
|
158
|
+
if (match) {
|
|
159
|
+
const aliasName = match[1];
|
|
160
|
+
const rest = match[2] ?? '';
|
|
161
|
+
for (const base of resolveAlias(aliasName, documentFormats))
|
|
162
|
+
out.push(base + rest);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
out.push(expression);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Document } from './document.js';
|
|
2
|
+
import type { Ruleset } from './ruleset.js';
|
|
3
|
+
import { type IDiagnostic, type ISourceSet } from './types.js';
|
|
4
|
+
/** Optional inputs to a runner's `run`: the dereferenced tree and its source documents. */
|
|
5
|
+
export type IRunOptions = {
|
|
6
|
+
/** Dereferenced document data, used by rules with `resolved: true`. */
|
|
7
|
+
resolved?: unknown;
|
|
8
|
+
/**
|
|
9
|
+
* The source documents that fed the resolved tree, keyed by absolute location.
|
|
10
|
+
* When provided, findings on nodes inlined from external (`$ref`'d) files map
|
|
11
|
+
* back to that file's own line:column and `source` instead of the `$ref` site
|
|
12
|
+
* in the root document. Omit for in-memory / internal-only resolution.
|
|
13
|
+
*/
|
|
14
|
+
sources?: ISourceSet;
|
|
15
|
+
};
|
|
16
|
+
/** Evaluates a normalized ruleset against a document. Produced by {@link createLinter}. */
|
|
17
|
+
export type Linter = {
|
|
18
|
+
run(document: Document, options?: IRunOptions): IDiagnostic[];
|
|
19
|
+
};
|
|
20
|
+
/** Creates a {@link Linter} runner bound to a normalized `ruleset`. */
|
|
21
|
+
export declare const createLinter: (ruleset: Ruleset) => Linter;
|