madr-lint 0.2.0 → 0.4.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/CHANGELOG.md +51 -0
- package/README.md +4 -2
- package/dist/cli.js +1534 -627
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +189 -5
- package/dist/index.js +460 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,38 @@ type MdastNodeType = Root['type'] | Nodes['type'];
|
|
|
5
5
|
type MdastNode = Root | Nodes;
|
|
6
6
|
/** Severity levels supported by every rule. */
|
|
7
7
|
type Severity = 'error' | 'warn';
|
|
8
|
+
/**
|
|
9
|
+
* A raw-text edit: replace `content.slice(range[0], range[1])` with `text`.
|
|
10
|
+
* Offsets are into the ORIGINAL WHOLE-FILE content (frontmatter included) —
|
|
11
|
+
* the applier's coordinate space. Rules never build these directly; they call
|
|
12
|
+
* the `Fixer` helpers, which translate body (mdast) offsets to whole-file
|
|
13
|
+
* offsets. See ADR-0008.
|
|
14
|
+
*/
|
|
15
|
+
interface TextEdit {
|
|
16
|
+
range: readonly [number, number];
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Helpers handed to a rule's `fix` thunk. Range / offset arguments are in
|
|
21
|
+
* BODY (mdast) coordinates — the same space as `node.position.*.offset` — and
|
|
22
|
+
* the fixer adds the stripped-frontmatter length so the returned `TextEdit`
|
|
23
|
+
* carries whole-file offsets. Keep the surface minimal: replace, insert, remove.
|
|
24
|
+
*/
|
|
25
|
+
interface Fixer {
|
|
26
|
+
/** Replace the body range with `text`. */
|
|
27
|
+
replaceRange(range: readonly [number, number], text: string): TextEdit;
|
|
28
|
+
/** Insert `text` at a body offset (zero-width edit). */
|
|
29
|
+
insertAt(offset: number, text: string): TextEdit;
|
|
30
|
+
/** Delete the body range. */
|
|
31
|
+
remove(range: readonly [number, number]): TextEdit;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A lazy fix producer. Invoked ONLY when fixing is requested (zero cost on a
|
|
35
|
+
* normal lint). Returns one edit, several, or `null` to decline (e.g. the
|
|
36
|
+
* concrete fix is not mechanically safe for this occurrence). A rule may only
|
|
37
|
+
* attach a fix when its `meta.fixable === 'code'`.
|
|
38
|
+
*/
|
|
39
|
+
type FixFn = (fixer: Fixer) => TextEdit | TextEdit[] | null;
|
|
8
40
|
/**
|
|
9
41
|
* The reserved rule name used for internal-error diagnostics. A real rule
|
|
10
42
|
* cannot register with this name (the registry should reject collisions).
|
|
@@ -33,6 +65,21 @@ interface RuleMeta<TOptions = Record<string, unknown>> {
|
|
|
33
65
|
};
|
|
34
66
|
/** messageId -> template string with {{placeholders}}. */
|
|
35
67
|
messages: Record<string, string>;
|
|
68
|
+
/**
|
|
69
|
+
* messageId -> machine-actionable remediation template, parallel to
|
|
70
|
+
* `messages` and interpolated the same way. Partial: declare an entry only
|
|
71
|
+
* where a concrete, mechanical fix exists; omit where the fix is inherently
|
|
72
|
+
* contextual. The runner resolves the entry for a reported messageId into
|
|
73
|
+
* `Diagnostic.suggestion` (null when absent). See issue #67.
|
|
74
|
+
*/
|
|
75
|
+
suggestions?: Record<string, string>;
|
|
76
|
+
/**
|
|
77
|
+
* Whether this rule can produce autofixes. `'code'` = the rule may attach a
|
|
78
|
+
* `fix` thunk to `context.report(...)`; the runner honors it only for
|
|
79
|
+
* fixable rules and routes a fix from a non-fixable rule through
|
|
80
|
+
* `core/internal-error` (a rule bug). Defaults to `false`. See ADR-0008.
|
|
81
|
+
*/
|
|
82
|
+
fixable?: 'code' | false;
|
|
36
83
|
/** Default options merged with user config. */
|
|
37
84
|
defaultOptions: TOptions;
|
|
38
85
|
/** AJV JSON Schema for options. Validated at runRule time. */
|
|
@@ -60,6 +107,33 @@ interface Diagnostic {
|
|
|
60
107
|
};
|
|
61
108
|
/** Data for placeholder interpolation in messages[messageId]. */
|
|
62
109
|
data?: Record<string, unknown>;
|
|
110
|
+
/**
|
|
111
|
+
* Machine-actionable remediation, resolved by the runner at report time from
|
|
112
|
+
* `rule.meta.suggestions[messageId]` (interpolated with `data`, exactly like
|
|
113
|
+
* the message). `null` when the rule declares no suggestion for this
|
|
114
|
+
* messageId. Every runner-produced diagnostic sets this. See issue #67.
|
|
115
|
+
*/
|
|
116
|
+
suggestion: string | null;
|
|
117
|
+
/**
|
|
118
|
+
* The rule's documentation URL (`rule.meta.docs.url`), resolved by the runner
|
|
119
|
+
* so consumers need not reconstruct it from `ruleName`. `core/internal-error`
|
|
120
|
+
* diagnostics point at the repository; a rule without a docs URL yields `''`.
|
|
121
|
+
*/
|
|
122
|
+
docsUrl: string;
|
|
123
|
+
/**
|
|
124
|
+
* Whether an autofix is available for THIS diagnostic — the rule is
|
|
125
|
+
* `meta.fixable: 'code'` AND attached a `fix` thunk here. Durable (serialized
|
|
126
|
+
* to the cache and json output; reporters render a 🔧 marker). Every
|
|
127
|
+
* runner-produced diagnostic sets this. See ADR-0008 / issue #28.
|
|
128
|
+
*/
|
|
129
|
+
fixable: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The live fix producer, present only when `fixable` is true. TRANSIENT — a
|
|
132
|
+
* closure, so it is dropped by JSON serialization (cache / json output) and
|
|
133
|
+
* absent on cache-hydrated diagnostics. Consumed by the autofix applier
|
|
134
|
+
* (`src/core/fix.ts`) after suppression + baseline filtering. See ADR-0008.
|
|
135
|
+
*/
|
|
136
|
+
fix?: FixFn;
|
|
63
137
|
}
|
|
64
138
|
interface RuleContext<TOptions = Record<string, unknown>> {
|
|
65
139
|
/** The file being linted. */
|
|
@@ -86,10 +160,27 @@ interface RuleContext<TOptions = Record<string, unknown>> {
|
|
|
86
160
|
line: number;
|
|
87
161
|
column: number;
|
|
88
162
|
}> | null;
|
|
163
|
+
/**
|
|
164
|
+
* Body-coordinate offset RANGE of the effective value for `metadata` keys
|
|
165
|
+
* whose value came from the v2 leading list AND was a single contiguous text
|
|
166
|
+
* token (no inline markup). Autofix uses this to target the value precisely
|
|
167
|
+
* (e.g. rewrite the status value in place). Frontmatter-sourced keys are
|
|
168
|
+
* absent — frontmatter is stripped before mdast parsing, so it has no body
|
|
169
|
+
* offset and needs YAML-aware rewriting instead. null when nothing qualifies.
|
|
170
|
+
* See ADR-0008.
|
|
171
|
+
*/
|
|
172
|
+
metadataValueLoc: Record<string, {
|
|
173
|
+
start: number;
|
|
174
|
+
end: number;
|
|
175
|
+
}> | null;
|
|
89
176
|
/** User-merged options for this rule (validated against rule.meta.schema). */
|
|
90
177
|
options: TOptions;
|
|
91
|
-
/**
|
|
92
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Emit a diagnostic. `suggestion` / `docsUrl` / `fixable` are resolved by the
|
|
180
|
+
* runner. A rule may pass a `fix` thunk ONLY when its `meta.fixable` is
|
|
181
|
+
* `'code'` (otherwise the runner routes it through `core/internal-error`).
|
|
182
|
+
*/
|
|
183
|
+
report(diagnostic: Omit<Diagnostic, 'ruleName' | 'severity' | 'path' | 'suggestion' | 'docsUrl' | 'fixable'>): void;
|
|
93
184
|
}
|
|
94
185
|
/**
|
|
95
186
|
* Visitor map dispatched during single-pass mdast traversal.
|
|
@@ -156,9 +247,9 @@ interface ProjectRuleContext<TOptions = Record<string, unknown>> {
|
|
|
156
247
|
/**
|
|
157
248
|
* Emit a diagnostic. Unlike per-file rules, project rules MUST set
|
|
158
249
|
* `path` explicitly — the runner cannot infer which file the
|
|
159
|
-
* diagnostic relates to.
|
|
250
|
+
* diagnostic relates to. `suggestion` / `docsUrl` are resolved by the runner.
|
|
160
251
|
*/
|
|
161
|
-
report(diagnostic: Omit<Diagnostic, 'ruleName' | 'severity'>): void;
|
|
252
|
+
report(diagnostic: Omit<Diagnostic, 'ruleName' | 'severity' | 'suggestion' | 'docsUrl' | 'fixable'>): void;
|
|
162
253
|
}
|
|
163
254
|
/**
|
|
164
255
|
* A cross-file rule. Receives all parsed files at once and reports
|
|
@@ -177,6 +268,79 @@ type AnyRule<TOptions = Record<string, unknown>> = Rule<TOptions> | ProjectRule<
|
|
|
177
268
|
/** Type guard distinguishing project rules from per-file rules. */
|
|
178
269
|
declare function isProjectRule(rule: AnyRule): rule is ProjectRule;
|
|
179
270
|
|
|
271
|
+
/** Fixpoint iteration cap (matches ESLint's autofix passes). */
|
|
272
|
+
declare const MAX_FIX_PASSES = 10;
|
|
273
|
+
/**
|
|
274
|
+
* Build a `Fixer` whose helpers take BODY offsets and emit whole-file
|
|
275
|
+
* `TextEdit`s by adding `frontmatterOffset` (the length gray-matter stripped).
|
|
276
|
+
*/
|
|
277
|
+
declare function makeFixer(base: number): Fixer;
|
|
278
|
+
/**
|
|
279
|
+
* Apply text edits to `content` in a single pass. Edits are sorted by start
|
|
280
|
+
* offset; any edit overlapping a previously-applied one is dropped (first wins).
|
|
281
|
+
* Invalid ranges (non-integer, negative, inverted, out of bounds) are dropped.
|
|
282
|
+
* Whole-file offsets in, new string out.
|
|
283
|
+
*/
|
|
284
|
+
declare function applyEdits(content: string, edits: readonly TextEdit[]): string;
|
|
285
|
+
/**
|
|
286
|
+
* Counting variant of `applyEdits`: `applied` is the number of edits that
|
|
287
|
+
* actually LANDED (post overlap/bounds filtering) — the honest source for
|
|
288
|
+
* `summary.fixed`, which must not count dropped edits. Exported for the
|
|
289
|
+
* cross-file fixpoint (`lintAndFix`), which applies project-rule edits per file
|
|
290
|
+
* and needs the landed count.
|
|
291
|
+
*/
|
|
292
|
+
declare function applyEditsCounted(content: string, edits: readonly TextEdit[]): {
|
|
293
|
+
text: string;
|
|
294
|
+
applied: number;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Invoke each diagnostic's `fix` thunk (if any) with a `Fixer` built for
|
|
298
|
+
* `content`, flattening arrays and skipping thunks that decline (return null).
|
|
299
|
+
* The frontmatter offset is derived from `content` so translation always
|
|
300
|
+
* matches the exact bytes being edited.
|
|
301
|
+
*/
|
|
302
|
+
declare function collectFixes(diagnostics: readonly Diagnostic[], content: string): TextEdit[];
|
|
303
|
+
/**
|
|
304
|
+
* Collect CROSS-FILE (project-rule) fixes, grouped by the file each edits.
|
|
305
|
+
* A project diagnostic names the file it targets via `diagnostic.path`; its fix
|
|
306
|
+
* emits an edit for THAT file. Unlike per-file fixes, project fixes operate in
|
|
307
|
+
* WHOLE-FILE coordinates: they may edit YAML frontmatter, which body (mdast)
|
|
308
|
+
* coordinates strip, so there is no frontmatter shift to add — the fixer's base
|
|
309
|
+
* is 0. See #29 (ADR-0008's per-file edit-set seam).
|
|
310
|
+
*
|
|
311
|
+
* `contentByPath(path)` returns the current content of a target file, or
|
|
312
|
+
* undefined if it is not in the fix set (its edits are then skipped). At most
|
|
313
|
+
* ONE fix contributes per file per pass — a second insertion at the same spot
|
|
314
|
+
* would duplicate a key; the fixpoint re-runs the project pass so the runners-up
|
|
315
|
+
* re-evaluate against the now-edited file (and typically decline).
|
|
316
|
+
*/
|
|
317
|
+
declare function collectProjectFixes(diagnostics: readonly Diagnostic[], contentByPath: (path: string) => string | undefined): Map<string, TextEdit[]>;
|
|
318
|
+
interface FixFileResult {
|
|
319
|
+
/** Content after the fixpoint loop (unchanged if nothing applied). */
|
|
320
|
+
fixedContent: string;
|
|
321
|
+
/** Diagnostics of the FINAL content (post-suppression / -baseline via `lint`). */
|
|
322
|
+
remaining: Diagnostic[];
|
|
323
|
+
/** Whether any edit actually changed the content. */
|
|
324
|
+
changed: boolean;
|
|
325
|
+
/** Number of applied passes (0 when nothing was fixed). */
|
|
326
|
+
passes: number;
|
|
327
|
+
/** Edits that actually LANDED across all passes (dropped edits excluded). */
|
|
328
|
+
applied: number;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Drive the autofix fixpoint for ONE file. `lint(content)` must return the
|
|
332
|
+
* REPORTED diagnostics for `content` — already suppression- and
|
|
333
|
+
* baseline-filtered — carrying live `fix` thunks. The loop lints at the top of
|
|
334
|
+
* every iteration, so `remaining` always reflects the returned `fixedContent`.
|
|
335
|
+
* Stops when: no edits remain, an edit makes no progress, or `maxPasses` is hit.
|
|
336
|
+
*/
|
|
337
|
+
declare function fixFileContent(originalContent: string, lint: (content: string) => Diagnostic[], maxPasses?: number): FixFileResult;
|
|
338
|
+
/**
|
|
339
|
+
* Produce a unified diff between `before` and `after` for display. Returns an
|
|
340
|
+
* empty string when the two are identical.
|
|
341
|
+
*/
|
|
342
|
+
declare function unifiedDiff(path: string, before: string, after: string): string;
|
|
343
|
+
|
|
180
344
|
/**
|
|
181
345
|
* Thrown when a rule's merged options fail AJV validation against
|
|
182
346
|
* `rule.meta.schema`. Distinct from generic `Error` so callers can
|
|
@@ -284,11 +448,31 @@ interface ParsedFile {
|
|
|
284
448
|
* effective value is list-sourced.
|
|
285
449
|
*/
|
|
286
450
|
metadataLoc: Record<string, MetadataPosition> | null;
|
|
451
|
+
/**
|
|
452
|
+
* Body-coordinate offset RANGE of the effective value for keys whose value
|
|
453
|
+
* came from the v2 leading list AND was a single contiguous text token (no
|
|
454
|
+
* inline markup), verified to slice back to the exact value. Autofix targets
|
|
455
|
+
* the value precisely with this. Frontmatter-won keys are absent (frontmatter
|
|
456
|
+
* is stripped before parsing → no body offset). null when nothing qualifies.
|
|
457
|
+
*/
|
|
458
|
+
metadataValueLoc: Record<string, {
|
|
459
|
+
start: number;
|
|
460
|
+
end: number;
|
|
461
|
+
}> | null;
|
|
287
462
|
/** mdast root of the body (frontmatter stripped). */
|
|
288
463
|
ast: Root;
|
|
289
464
|
/** Body content with frontmatter stripped. */
|
|
290
465
|
body: string;
|
|
291
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Character length of the frontmatter block that gray-matter strips from the
|
|
469
|
+
* front of `content` — i.e. the offset at which the body begins in the whole
|
|
470
|
+
* file. mdast positions are body-relative, so `fileOffset = bodyOffset +
|
|
471
|
+
* frontmatterOffset(content)`. gray-matter never mutates the body, so the body
|
|
472
|
+
* is always an exact suffix and this length is exact (verified across CRLF and
|
|
473
|
+
* leading-newline inputs). Used by the autofix fixer. See ADR-0008.
|
|
474
|
+
*/
|
|
475
|
+
declare function frontmatterOffset(content: string): number;
|
|
292
476
|
/**
|
|
293
477
|
* Parse a Markdown file: extract YAML frontmatter via gray-matter, then
|
|
294
478
|
* feed the body into mdast-util-from-markdown directly (per ADR-0002,
|
|
@@ -458,4 +642,4 @@ interface MadrLintConfig {
|
|
|
458
642
|
*/
|
|
459
643
|
declare function defineConfig(config: MadrLintConfig): MadrLintConfig;
|
|
460
644
|
|
|
461
|
-
export { type AnyRule, BASELINE_VERSION, type Baseline, type BaselineApplyResult, type Diagnostic, type FileContext, INTERNAL_ERROR_RULE_NAME, type MadrLintConfig, type MadrVersion, type MdastNode, type MdastNodeType, type ParsedFile, type ProjectFile, type ProjectRule, type ProjectRuleContext, type Rule, type RuleContext, type RuleListeners, type RuleMeta, RuleOptionsError, type RuleSeverity, type RunRuleOptions, type Severity, applyBaseline, baselinePath, buildBaseline, buildProjectFile, defineConfig, extractListMetadata, isProjectRule, loadBaseline, parseFile, recommended, index as rules, runRule, runRulesOnFile, runRulesOnProject, serializeBaseline, writeBaseline };
|
|
645
|
+
export { type AnyRule, BASELINE_VERSION, type Baseline, type BaselineApplyResult, type Diagnostic, type FileContext, type FixFileResult, type FixFn, type Fixer, INTERNAL_ERROR_RULE_NAME, MAX_FIX_PASSES, type MadrLintConfig, type MadrVersion, type MdastNode, type MdastNodeType, type ParsedFile, type ProjectFile, type ProjectRule, type ProjectRuleContext, type Rule, type RuleContext, type RuleListeners, type RuleMeta, RuleOptionsError, type RuleSeverity, type RunRuleOptions, type Severity, type TextEdit, applyBaseline, applyEdits, applyEditsCounted, baselinePath, buildBaseline, buildProjectFile, collectFixes, collectProjectFixes, defineConfig, extractListMetadata, fixFileContent, frontmatterOffset, isProjectRule, loadBaseline, makeFixer, parseFile, recommended, index as rules, runRule, runRulesOnFile, runRulesOnProject, serializeBaseline, unifiedDiff, writeBaseline };
|