@markuplint/ml-core 5.0.0-alpha.1 → 5.0.0-alpha.3

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.
Files changed (46) hide show
  1. package/ARCHITECTURE.ja.md +158 -5
  2. package/ARCHITECTURE.md +209 -6
  3. package/CHANGELOG.md +25 -0
  4. package/docs/ml-dom/document.ja.md +7 -14
  5. package/docs/ml-dom/document.md +7 -14
  6. package/docs/ml-dom/element.ja.md +9 -24
  7. package/docs/ml-dom/element.md +9 -24
  8. package/docs/rule-system.ja.md +1 -1
  9. package/docs/rule-system.md +1 -1
  10. package/lib/cursor-offset.d.ts +13 -0
  11. package/lib/cursor-offset.js +34 -0
  12. package/lib/fix-applier.d.ts +32 -0
  13. package/lib/fix-applier.js +75 -0
  14. package/lib/index.d.ts +3 -0
  15. package/lib/index.js +2 -0
  16. package/lib/ml-core.d.ts +53 -8
  17. package/lib/ml-core.js +197 -59
  18. package/lib/ml-dom/helper/get-indent.d.ts +0 -1
  19. package/lib/ml-dom/helper/get-indent.js +5 -18
  20. package/lib/ml-dom/node/attr.d.ts +2 -13
  21. package/lib/ml-dom/node/attr.js +3 -35
  22. package/lib/ml-dom/node/block.js +2 -1
  23. package/lib/ml-dom/node/character-data.d.ts +35 -0
  24. package/lib/ml-dom/node/character-data.js +35 -6
  25. package/lib/ml-dom/node/document.d.ts +2 -27
  26. package/lib/ml-dom/node/document.js +7 -46
  27. package/lib/ml-dom/node/dom-token-list.d.ts +0 -1
  28. package/lib/ml-dom/node/dom-token-list.js +3 -3
  29. package/lib/ml-dom/node/element-close-tag.d.ts +1 -1
  30. package/lib/ml-dom/node/element-close-tag.js +2 -16
  31. package/lib/ml-dom/node/element.d.ts +2 -19
  32. package/lib/ml-dom/node/element.js +3 -65
  33. package/lib/ml-dom/token/token.d.ts +3 -18
  34. package/lib/ml-dom/token/token.js +7 -28
  35. package/lib/ml-rule/index.d.ts +1 -0
  36. package/lib/ml-rule/index.js +1 -0
  37. package/lib/ml-rule/ml-rule-context.d.ts +2 -31
  38. package/lib/ml-rule/ml-rule-context.js +18 -18
  39. package/lib/ml-rule/ml-rule.d.ts +6 -11
  40. package/lib/ml-rule/ml-rule.js +36 -37
  41. package/lib/ml-rule/rule-fixer.d.ts +20 -0
  42. package/lib/ml-rule/rule-fixer.js +38 -0
  43. package/lib/ml-rule/types.d.ts +1 -2
  44. package/lib/test/index.d.ts +1 -10
  45. package/lib/test/index.js +0 -11
  46. package/package.json +12 -12
@@ -49,9 +49,11 @@ src/
49
49
  ├── ml-rule/
50
50
  │ ├── ml-rule.ts — MLRule クラス(ルール実行)
51
51
  │ ├── ml-rule-context.ts — MLRuleContext(レポート収集)
52
+ │ ├── rule-fixer.ts — RuleFixer(fix コールバック用の TextEdit ビルダー)
52
53
  │ ├── create-rule.ts — createRule ファクトリ
53
54
  │ ├── create-test-rule.ts — テスト用ルールファクトリ
54
55
  │ └── types.ts — RuleSeed, Checker 型
56
+ ├── fix-applier.ts — applyFixes(重複検出付き TextEdit 適用エンジン)
55
57
  ├── ruleset/
56
58
  │ └── index.ts — Ruleset クラス(rules + nodeRules + childNodeRules)
57
59
  ├── plugin/
@@ -143,8 +145,8 @@ flowchart LR
143
145
 
144
146
  1. **パース**: `MLCore` は設定されたパーサー(`MLParser`)を呼び出し、`MLASTDocument` を生成
145
147
  2. **ドキュメント作成**: AST を `MLDocument` でラップし、`createNode()` ファクトリで MLDOM ツリー全体を構築。`RuleMapper` が各ノードのルール設定を解決
146
- 3. **検証**: 各 `MLRule` に対して、`document.setRule(rule)` を呼び出した後 `rule.verify(document)` を実行。ルールは `document.walkOn()` で対象ノードを走査し、`MLRuleContext` を通じて違反を報告
147
- 4. **修正**(オプション): `fix=true` の場合、ルールが `node.fix()` でトークン内容を変更。`document.toString(true)` で修正後のソースを生成
148
+ 3. **検証**: 各 `MLRule` に対して、`document.setRule(rule)` を呼び出した後 `rule.verify(document)` を実行。ルールは `document.walkOn()` で対象ノードを走査し、`MLRuleContext` を通じて違反を報告。ルールは report にインライン `fix` コールバックを付与して `TextEdit` オブジェクトを返す
149
+ 4. **修正**(オプション): `fix=true` の場合、report fix コールバックが `RuleFixer` を使って `TextEdit[]` を生成。`FixApplier.applyFixes(sourceCode, fixes)` が全編集をソーステキストに一括適用(重複検出付き)
148
150
 
149
151
  ## MLDOM クラス階層
150
152
 
@@ -210,10 +212,9 @@ MLToken<A extends MLASTToken>
210
212
  | --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
211
213
  | `walkOn(type, walker)` | 指定した型(`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`)のノードを走査 |
212
214
  | `setRule(rule)` | 現在のルールを設定(検証時に `MLCore` が使用) |
213
- | `getTokenList()` | ソース再構築用の全トークンを返す |
214
215
  | `searchNodeByLocation(line, col)` | 指定したソース位置のノードを検索 |
215
216
  | `getAccessibilityProp(node)` | ARIA アクセシビリティプロパティを計算(`MLElement.getAccessibleName()` のキャッシュ経由でアクセシブルネームを取得) |
216
- | `toString(fixed?)` | ソースコードを再構築(オプションで修正適用) |
217
+ | `toString()` | ドキュメントの生のソースコードを返す |
217
218
 
218
219
  ## MLElement
219
220
 
@@ -292,7 +293,7 @@ type RuleSeed<T, O> = {
292
293
  - `translate` / `t` — ロケール対応のメッセージ翻訳
293
294
  - `report(report)` — ノード、メッセージ、オプションの修正とともに違反を報告
294
295
 
295
- `provide()` メソッドは `RuleSeed.verify()` `RuleSeed.fix()` に渡されるコンテキストオブジェクトを返します。
296
+ `provide()` メソッドは `RuleSeed.verify()` に渡されるコンテキストオブジェクトを返します。自動修正ロジックは、個々の `report()` 呼び出しのインライン `fix` コールバックとして提供され、独立したライフサイクルメソッドではありません。
296
297
 
297
298
  ### ルール設定の解決
298
299
 
@@ -367,6 +368,158 @@ Named nodeRule(設定) 仮想 MLRule(ランタイム)
367
368
  2. **グループ無効化**: `rules["custom/multi"]: false`(複数エントリの named nodeRule 用)
368
369
  3. **名前空間ワイルドカード**: `rules["a11y/*"]: false`(`a11y/` で始まるすべての仮想ルールを無効化)
369
370
 
371
+ ## 自動修正(Autofix)システム
372
+
373
+ 自動修正システムは、ルールが違反に対する自動修正を提供する仕組みです。**RuleFixer**(TextEdit ビルダー)、**fix コールバック**(ルール作者が記述するロジック)、**FixApplier**(編集適用エンジン)の 3 つのコンポーネントで構成されます。
374
+
375
+ ### 自動修正のデータフロー
376
+
377
+ ```mermaid
378
+ flowchart LR
379
+ subgraph RulePhase ["ルールフェーズ"]
380
+ report["context.report({\n message,\n scope,\n fix: コールバック\n})"]
381
+ end
382
+
383
+ subgraph FixPhase ["Fix コールバック実行"]
384
+ callback["fix(fixer) → TextEdit[]"]
385
+ fixer["RuleFixer\n(共有インスタンス)"]
386
+ callback --> fixer
387
+ end
388
+
389
+ subgraph ApplyPhase ["適用フェーズ"]
390
+ fixdata["FixData\n{ edits: TextEdit[] }"]
391
+ applier["applyFixes(\n sourceCode,\n allFixes\n)"]
392
+ output["fixedCode"]
393
+ fixdata --> applier --> output
394
+ end
395
+
396
+ report --> callback
397
+ fixer --> fixdata
398
+ ```
399
+
400
+ ### Fix コールバックの動作
401
+
402
+ ルールは各 `report()` 呼び出しにオプションの `fix` コールバックを付与します。このコールバックはルール検証中には**実行されず**、保存されるだけです。`MLCore.verify()` に `fix=true` が渡された場合にのみ呼び出されます。
403
+
404
+ ```mermaid
405
+ sequenceDiagram
406
+ participant Rule as ルール (verify)
407
+ participant Ctx as MLRuleContext
408
+ participant MLR as MLRule.verify()
409
+ participant Fixer as RuleFixer
410
+ participant Core as MLCore.verify()
411
+ participant FA as applyFixes()
412
+
413
+ Rule->>Ctx: report({ scope, message, fix })
414
+ Note over Ctx: fix コールバック付きレポートを格納
415
+
416
+ MLR->>Ctx: context.reports
417
+ loop fix コールバックを持つ各レポート
418
+ MLR->>Fixer: report.fix(sharedFixer)
419
+ Fixer-->>MLR: TextEdit | TextEdit[]
420
+ MLR->>MLR: FixData { edits } としてラップ
421
+ end
422
+ MLR-->>Core: Violation[](FixData 付き)
423
+
424
+ Core->>Core: 全 Violation から FixData を収集
425
+ Core->>FA: applyFixes(sourceCode, allFixes)
426
+ FA-->>Core: FixResult { output, applied, skipped }
427
+ ```
428
+
429
+ ### RuleFixer API
430
+
431
+ `RuleFixer` は `IRuleFixer`(`@markuplint/ml-config` で定義)を実装します。**ステートレス**なヘルパーであり、全ルールで 1 つのインスタンスを共有します。各メソッドはソースコードのレンジ置換を記述する `TextEdit` オブジェクトを生成します。
432
+
433
+ | メソッド | 入力 | 生成される TextEdit |
434
+ | --------------------------- | ------------------------------------ | --------------------------------------- |
435
+ | `replaceText(token, text)` | `startOffset` + `raw` を持つトークン | `range: [start, start+len], text` |
436
+ | `replaceRange(range, text)` | 明示的な `[start, end)` レンジ | `range: [start, end], text` |
437
+ | `insertBefore(token, text)` | `startOffset` を持つトークン | `range: [start, start], text`(ゼロ幅) |
438
+ | `insertAfter(token, text)` | `startOffset` + `raw` を持つトークン | `range: [end, end], text`(ゼロ幅) |
439
+ | `remove(token)` | `startOffset` + `raw` を持つトークン | `range: [start, start+len], text: ""` |
440
+ | `removeRange(range)` | 明示的な `[start, end)` レンジ | `range: [start, end], text: ""` |
441
+
442
+ `token` パラメータは `FixToken` 型(`@markuplint/ml-config` で定義)を満たす任意のオブジェクト — つまり `{ startOffset: number; raw: string }` — を受け付けます。MLDOM トークン(`MLToken`, `MLAttr` 等)は自然にこの要件を満たします。
443
+
444
+ ### FixApplier アルゴリズム
445
+
446
+ `applyFixes()`(`fix-applier.ts`)は全ルールの `FixData` をマージし、1 パスで適用します:
447
+
448
+ ```mermaid
449
+ flowchart TD
450
+ A["展開: FixData[] → タグ付き TextEdit[]"]
451
+ B["ソート: range start 昇順、\nthen range end 降順"]
452
+ C["逐次適用:\n各 edit の重複をチェック"]
453
+ D{{"edit.start < lastAppliedEnd?"}}
454
+ E["スキップ\n(親 FixData をスキップとしてマーク)"]
455
+ F["適用\n(出力にスプライス)"]
456
+ G["分類: 各 FixData を\napplied または skipped に"]
457
+ H["FixResult を返す\n{ output, applied, skipped }"]
458
+
459
+ A --> B --> C --> D
460
+ D -- Yes --> E --> C
461
+ D -- No --> F --> C
462
+ C -. "全 edit 処理完了" .-> G --> H
463
+ ```
464
+
465
+ 主要な制約:
466
+
467
+ - 1 つの `FixData` 内の edit は互いに重複してはならない
468
+ - `FixData` 間の重複はスキップ機構で処理される
469
+ - `FixData` 内のいずれかの edit がスキップされると、その `FixData` 全体がスキップとして分類される
470
+
471
+ ### マルチパス Fix ループ
472
+
473
+ `applyFixes()` がレンジの重複により一部の fix をスキップした場合、エンジンはマルチパスループ(`_multiPassFix()`)に入り、再パース・再検証を繰り返して修正可能な違反をすべて解決します:
474
+
475
+ ```mermaid
476
+ flowchart TD
477
+ A["violations から fix を抽出"] --> B["applyFixes(code, fixes)"]
478
+ B --> C{"applied.length === 0?"}
479
+ C -- Yes --> Z["現在のコードを返す"]
480
+ C -- No --> D{"output === currentCode?"}
481
+ D -- Yes --> Z
482
+ D -- No --> E{"サイクル検出?\n(2パス前の出力と一致)"}
483
+ E -- Yes --> Z
484
+ E -- No --> F{"skipped.length === 0?"}
485
+ F -- Yes --> Z["修正済みコードを返す\n(全 fix 適用完了)"]
486
+ F -- No --> G["再パース + 再検証"]
487
+ G --> H{"ParserError?"}
488
+ H -- Yes --> Z["最後の正常なコードに戻す"]
489
+ H -- No --> I{"新たな修正可能な違反?"}
490
+ I -- No --> Z
491
+ I -- Yes --> B
492
+ ```
493
+
494
+ 主な設計ポイント:
495
+
496
+ - **ゼロコストパス**: fix を持つ violation がなければ、マルチパスループは完全にスキップされる
497
+ - **シングルパス高速パス**: `skipped.length === 0` のとき即座にループを抜ける(Phase 1 と同等の動作)
498
+ - **サイクル検出**: 2パス前の出力と比較し、A→B→A の振動パターンを検出
499
+ - **安全上限**: 最大10パス(ESLint の `SourceCodeFixer` と同じ)
500
+ - **状態復元**: `verify()` は `try/finally` で `#sourceCode`、`#ast`、`#document` を保存・復元
501
+
502
+ **重要**: `VerifyResult` の `violations` 配列は初回パスの結果のみを反映し、`fixedCode` は複数パスの結果である場合があります。修正後コードの正確な違反リストが必要な場合は、出力を再検証してください。
503
+
504
+ ### 実例: ルールの Fix 実装
505
+
506
+ ```typescript
507
+ // ルールの verify 関数内:
508
+ context.report({
509
+ scope: node,
510
+ message: '属性値にはダブルクォートを使用してください',
511
+ fix: fixer => fixer.replaceText(node.attrValueToken, `"${value}"`),
512
+ });
513
+ ```
514
+
515
+ これにより以下の処理が行われます:
516
+
517
+ 1. **レポート** → `MLRuleContext` に格納
518
+ 2. **Fix コールバック** → `(fixer) => fixer.replaceText(token, text)`(まだ呼び出されない)
519
+ 3. **`fix=true` の場合** → 共有 `RuleFixer` でコールバック実行 → `TextEdit` を返す
520
+ 4. **TextEdit** → `FixData { edits: [{ range: [12, 17], text: '"hello"' }] }` としてラップ
521
+ 5. **applyFixes** → ソースコードに置換を適用
522
+
370
523
  ## Pretender システム
371
524
 
372
525
  pretender システムにより、コンポーネントをリンティング時にセマンティック HTML 要素として扱うことができます。これにより、ルールがカスタムコンポーネント(例:`<MyButton>`)を標準要素(例:`<button>`)として検証できます。
package/ARCHITECTURE.md CHANGED
@@ -49,9 +49,12 @@ src/
49
49
  ├── ml-rule/
50
50
  │ ├── ml-rule.ts — MLRule class (rule execution)
51
51
  │ ├── ml-rule-context.ts — MLRuleContext (report collection)
52
+ │ ├── rule-fixer.ts — RuleFixer (TextEdit builder for fix callbacks)
52
53
  │ ├── create-rule.ts — createRule factory
53
54
  │ ├── create-test-rule.ts — Test rule factory
54
55
  │ └── types.ts — RuleSeed, Checker types
56
+ ├── cursor-offset.ts — computeCursorOffset (cursor remapping after edits)
57
+ ├── fix-applier.ts — applyFixes (overlap-aware TextEdit applicator)
55
58
  ├── ruleset/
56
59
  │ └── index.ts — Ruleset class (rules + nodeRules + childNodeRules)
57
60
  ├── plugin/
@@ -131,7 +134,7 @@ flowchart LR
131
134
  A["MLCore\nconstructor"]
132
135
  B["_parse()\nParser → MLASTDocument"]
133
136
  C["_createDocument()\nMLASTDocument → MLDocument"]
134
- D["verify(fix?)\nFor each rule:"]
137
+ D["verify(fix? | options?)\nFor each rule:"]
135
138
  E["document.setRule(rule)\nRuleMapper maps config → nodes"]
136
139
  F["rule.verify(document)\nMLRuleContext collects reports"]
137
140
  G["Violations[]"]
@@ -143,8 +146,8 @@ flowchart LR
143
146
 
144
147
  1. **Parse**: `MLCore` invokes the configured parser (`MLParser`) to produce an `MLASTDocument`
145
148
  2. **Create Document**: The AST is wrapped in an `MLDocument`, which builds the full MLDOM tree via `createNode()` factory. `RuleMapper` resolves rule configuration for every node
146
- 3. **Verify**: For each `MLRule`, the engine calls `document.setRule(rule)` then `rule.verify(document)`. The rule walks relevant nodes via `document.walkOn()` and reports violations through `MLRuleContext`
147
- 4. **Fix** (optional): When `fix=true`, rules may call `node.fix()` to modify token content. `document.toString(true)` produces the fixed source
149
+ 3. **Verify**: For each `MLRule`, the engine calls `document.setRule(rule)` then `rule.verify(document)`. The rule walks relevant nodes via `document.walkOn()` and reports violations through `MLRuleContext`. Rules may attach inline `fix` callbacks to reports that return `TextEdit` objects
150
+ 4. **Fix** (optional): When `fix=true`, fix callbacks on reports are executed via `RuleFixer` to produce `TextEdit[]`. `FixApplier.applyFixes(sourceCode, fixes)` applies all edits to the source text with overlap detection. When fixes require multiple passes, `_multiPassFix()` orchestrates re-parsing and re-verification, returning a `FixSummary` with pass count, applied/skipped totals, and first-pass edits for cursor offset computation
148
151
 
149
152
  ## MLDOM Class Hierarchy
150
153
 
@@ -210,10 +213,9 @@ The constructor receives an `MLASTDocument`, a `Ruleset`, and an `MLSchema` tupl
210
213
  | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
211
214
  | `walkOn(type, walker)` | Walks nodes of a given type (`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`) |
212
215
  | `setRule(rule)` | Sets the current rule, used by `MLCore` during verification |
213
- | `getTokenList()` | Returns all tokens for source reconstruction |
214
216
  | `searchNodeByLocation(line, col)` | Finds the node at a given source position |
215
217
  | `getAccessibilityProp(node)` | Computes ARIA accessibility properties (delegates to `MLElement.getAccessibleName()` for cached accessible name) |
216
- | `toString(fixed?)` | Reconstructs source code, optionally with fixes applied |
218
+ | `toString()` | Returns the raw source code of the document |
217
219
 
218
220
  ## MLElement
219
221
 
@@ -292,7 +294,7 @@ type RuleSeed<T, O> = {
292
294
  - `translate` / `t` — Locale-aware message translator
293
295
  - `report(report)` — Reports a violation with node, message, and optional fix
294
296
 
295
- The `provide()` method returns the context object passed to `RuleSeed.verify()` and `RuleSeed.fix()`.
297
+ The `provide()` method returns the context object passed to `RuleSeed.verify()`. Auto-fix logic is provided as an inline `fix` callback on individual `report()` calls, not as a separate lifecycle method.
296
298
 
297
299
  ### Rule Configuration Resolution
298
300
 
@@ -367,6 +369,207 @@ Virtual rules can be disabled at three levels in the `rules` config:
367
369
  2. **Group disable**: `rules["custom/multi"]: false` (for multi-entry named nodeRules)
368
370
  3. **Namespace wildcard**: `rules["a11y/*"]: false` (disables all virtual rules starting with `a11y/`)
369
371
 
372
+ ## Autofix System
373
+
374
+ The autofix system allows rules to provide automatic fixes for violations. It operates through three components: **RuleFixer** (TextEdit builder), **fix callbacks** (rule-authored logic), and **FixApplier** (edit application engine).
375
+
376
+ ### Autofix Data Flow
377
+
378
+ ```mermaid
379
+ flowchart LR
380
+ subgraph RulePhase ["Rule Phase"]
381
+ report["context.report({\n message,\n scope,\n fix: callback\n})"]
382
+ end
383
+
384
+ subgraph FixPhase ["Fix Callback Execution"]
385
+ callback["fix(fixer) → TextEdit[]"]
386
+ fixer["RuleFixer\n(shared instance)"]
387
+ callback --> fixer
388
+ end
389
+
390
+ subgraph ApplyPhase ["Apply Phase"]
391
+ fixdata["FixData\n{ edits: TextEdit[] }"]
392
+ applier["applyFixes(\n sourceCode,\n allFixes\n)"]
393
+ output["fixedCode"]
394
+ fixdata --> applier --> output
395
+ end
396
+
397
+ report --> callback
398
+ fixer --> fixdata
399
+ ```
400
+
401
+ ### How Fix Callbacks Work
402
+
403
+ Rules attach an optional `fix` callback to each `report()` call. The callback is **not** executed during rule verification — it is stored and only invoked when `fix=true` is passed to `MLCore.verify()`.
404
+
405
+ ```mermaid
406
+ sequenceDiagram
407
+ participant Rule as Rule (verify)
408
+ participant Ctx as MLRuleContext
409
+ participant MLR as MLRule.verify()
410
+ participant Fixer as RuleFixer
411
+ participant Core as MLCore.verify()
412
+ participant FA as applyFixes()
413
+
414
+ Rule->>Ctx: report({ scope, message, fix })
415
+ Note over Ctx: Stores report with fix callback
416
+
417
+ MLR->>Ctx: context.reports
418
+ loop Each report with fix callback
419
+ MLR->>Fixer: report.fix(sharedFixer)
420
+ Fixer-->>MLR: TextEdit | TextEdit[]
421
+ MLR->>MLR: Wrap as FixData { edits }
422
+ end
423
+ MLR-->>Core: Violation[] (with FixData)
424
+
425
+ Core->>Core: Collect all FixData from violations
426
+ Core->>FA: applyFixes(sourceCode, allFixes)
427
+ FA-->>Core: FixResult { output, applied, skipped, appliedEdits }
428
+ ```
429
+
430
+ ### RuleFixer API
431
+
432
+ `RuleFixer` implements `IRuleFixer` (defined in `@markuplint/ml-config`). It is a **stateless** helper — a single instance is shared across all rules. Each method builds a `TextEdit` object describing a range replacement on the source code.
433
+
434
+ | Method | Input | TextEdit Produced |
435
+ | --------------------------- | -------------------------------- | ------------------------------------------ |
436
+ | `replaceText(token, text)` | Token with `startOffset` + `raw` | `range: [start, start+len], text` |
437
+ | `replaceRange(range, text)` | Explicit `[start, end)` range | `range: [start, end], text` |
438
+ | `insertBefore(token, text)` | Token with `startOffset` | `range: [start, start], text` (zero-width) |
439
+ | `insertAfter(token, text)` | Token with `startOffset` + `raw` | `range: [end, end], text` (zero-width) |
440
+ | `remove(token)` | Token with `startOffset` + `raw` | `range: [start, start+len], text: ""` |
441
+ | `removeRange(range)` | Explicit `[start, end)` range | `range: [start, end], text: ""` |
442
+
443
+ The `token` parameter accepts any object satisfying the `FixToken` type (defined in `@markuplint/ml-config`) — i.e., `{ startOffset: number; raw: string }`. MLDOM tokens (`MLToken`, `MLAttr`, etc.) satisfy this naturally.
444
+
445
+ ### FixApplier Algorithm
446
+
447
+ `applyFixes()` (in `fix-applier.ts`) merges all `FixData` from all rules and applies them in a single pass:
448
+
449
+ ```mermaid
450
+ flowchart TD
451
+ A["Flatten: FixData[] → tagged TextEdit[]"]
452
+ B["Sort: by range start ascending,\nthen range end descending"]
453
+ C["Apply sequentially:\nfor each edit, check overlap"]
454
+ D{{"edit.start < lastAppliedEnd?"}}
455
+ E["Skip edit\n(mark parent FixData as skipped)"]
456
+ F["Apply edit\n(splice into output)"]
457
+ G["Classify: each FixData as\napplied or skipped"]
458
+ H["Return FixResult\n{ output, applied, skipped, appliedEdits }"]
459
+
460
+ A --> B --> C --> D
461
+ D -- Yes --> E --> C
462
+ D -- No --> F --> C
463
+ C -. "all edits processed" .-> G --> H
464
+ ```
465
+
466
+ Key constraints:
467
+
468
+ - Edits within a single `FixData` must not overlap each other
469
+ - Inter-`FixData` overlap is handled by the skip mechanism
470
+ - If any edit in a `FixData` is skipped, the entire `FixData` is classified as skipped
471
+ - `appliedEdits` is a flat list of all successfully applied `TextEdit` objects, sorted by `range[0]` ascending — used for cursor offset computation
472
+
473
+ ### Multi-Pass Fix Loop
474
+
475
+ When `applyFixes()` skips some fixes due to range overlap, the engine enters a multi-pass loop (`_multiPassFix()`) that re-parses and re-verifies until all fixable violations are resolved:
476
+
477
+ ```mermaid
478
+ flowchart TD
479
+ A["Extract fixes from violations"] --> B["applyFixes(code, fixes)"]
480
+ B --> C{"applied.length === 0?"}
481
+ C -- Yes --> Z["Return current code"]
482
+ C -- No --> D{"output === currentCode?"}
483
+ D -- Yes --> Z
484
+ D -- No --> E{"Cycle detected?\n(output === code from 2 passes ago)"}
485
+ E -- Yes --> Z
486
+ E -- No --> F{"skipped.length === 0?"}
487
+ F -- Yes --> Z["Return fixed code\n(all fixes applied)"]
488
+ F -- No --> G["Re-parse + re-verify"]
489
+ G --> H{"ParserError?"}
490
+ H -- Yes --> Z["Revert to last good code"]
491
+ H -- No --> I{"New fixable violations?"}
492
+ I -- No --> Z
493
+ I -- Yes --> B
494
+ ```
495
+
496
+ Key design points:
497
+
498
+ - **Zero-cost path**: If no violations have fixes, the multi-pass loop is skipped entirely
499
+ - **Single-pass fast path**: When `skipped.length === 0`, the loop exits immediately — equivalent to Phase 1 behavior
500
+ - **Cycle detection**: Compares current output against the output from two passes ago to detect A→B→A oscillation
501
+ - **Safety cap**: Maximum 10 passes (same as ESLint's `SourceCodeFixer`)
502
+ - **State restoration**: `verify()` saves and restores `#sourceCode`, `#ast`, and `#document` via `try/finally`
503
+
504
+ **Important**: The `violations` array in `VerifyResult` reflects the first pass only, while `fixedCode` may be the result of multiple passes. Callers needing an accurate violation list for the fixed code should re-verify the output.
505
+
506
+ ### VerifyResult and FixSummary
507
+
508
+ `MLCore.verify()` accepts either a `boolean` or a `VerifyOptions` object:
509
+
510
+ ```typescript
511
+ verify(fix?: boolean): Promise<VerifyResult>;
512
+ verify(options?: VerifyOptions): Promise<VerifyResult>;
513
+ ```
514
+
515
+ `VerifyResult` contains:
516
+
517
+ | Field | Type | Description |
518
+ | ------------ | ------------------------- | ------------------------------------------------------------- |
519
+ | `violations` | `readonly Violation[]` | Violations from the first verification pass |
520
+ | `fixedCode` | `string \| undefined` | Source after all fix passes; `undefined` when fix is disabled |
521
+ | `fixSummary` | `FixSummary \| undefined` | Fix process summary; present when `fix=true` |
522
+
523
+ `FixSummary` provides diagnostics about the multi-pass fix process:
524
+
525
+ | Field | Type | Description |
526
+ | ------------------ | --------------------- | ---------------------------------------------------------------- |
527
+ | `passCount` | `number` | Number of fix passes executed |
528
+ | `totalApplied` | `number` | Total fixes applied across all passes |
529
+ | `totalSkipped` | `number` | Total fixes skipped (overlap) across all passes |
530
+ | `reachedMaxPasses` | `boolean` | Whether the 10-pass safety cap was reached |
531
+ | `firstPassEdits` | `readonly TextEdit[]` | Applied edits from the first pass only (original source offsets) |
532
+
533
+ `firstPassEdits` references the original source code offsets, making them suitable for cursor remapping via `computeCursorOffset()`.
534
+
535
+ ### Cursor Offset Computation
536
+
537
+ `computeCursorOffset()` (in `cursor-offset.ts`) maps a cursor position from the original source to the fixed source using the first-pass applied edits:
538
+
539
+ ```typescript
540
+ import { computeCursorOffset } from '@markuplint/ml-core';
541
+
542
+ const newOffset = computeCursorOffset(fixSummary.firstPassEdits, originalCursorOffset);
543
+ ```
544
+
545
+ Algorithm:
546
+
547
+ 1. Walk through edits sorted by `range[0]` ascending
548
+ 2. For each edit before the cursor: accumulate `delta = text.length - (end - start)`
549
+ 3. For edits after the cursor: stop (no effect)
550
+ 4. If the cursor falls inside a replaced range `[start, end)`: place at `start + text.length`
551
+
552
+ Ranges use half-open intervals: a cursor at position `end` is considered **outside** the edit.
553
+
554
+ ### Example: Rule Fix in Practice
555
+
556
+ ```typescript
557
+ // In a rule's verify function:
558
+ context.report({
559
+ scope: node,
560
+ message: 'Attribute value must use double quotes',
561
+ fix: fixer => fixer.replaceText(node.attrValueToken, `"${value}"`),
562
+ });
563
+ ```
564
+
565
+ This produces:
566
+
567
+ 1. **Report** → stored in `MLRuleContext`
568
+ 2. **Fix callback** → `(fixer) => fixer.replaceText(token, text)` (not yet called)
569
+ 3. **When `fix=true`** → callback invoked with shared `RuleFixer` → returns `TextEdit`
570
+ 4. **TextEdit** → wrapped as `FixData { edits: [{ range: [12, 17], text: '"hello"' }] }`
571
+ 5. **applyFixes** → splices the replacement into source code
572
+
370
573
  ## Pretender System
371
574
 
372
575
  The pretender system allows components to be treated as semantic HTML elements during linting. This enables rules to validate custom components (e.g., `<MyButton>`) as if they were standard elements (e.g., `<button>`).
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-alpha.3](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.2...v5.0.0-alpha.3) (2026-02-26)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** treat edits within a single FixData as atomic unit ([0bb980b](https://github.com/markuplint/markuplint/commit/0bb980b7cc6fc9b89a82f3d4df58b7137a6b8766))
11
+
12
+ ### Features
13
+
14
+ - **ml-core:** add cursor offset computation and fix summary metadata ([74b6e28](https://github.com/markuplint/markuplint/commit/74b6e28e4be2802e841697899f57f6ae04e4ffe9))
15
+ - **ml-core:** add multi-pass fix loop and cycle detection ([866b1d5](https://github.com/markuplint/markuplint/commit/866b1d54199ed1f1b5195cd0f61f3ee392b1d8a7))
16
+
17
+ # [5.0.0-alpha.2](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.1...v5.0.0-alpha.2) (2026-02-23)
18
+
19
+ ### Features
20
+
21
+ - **ml-core:** implement autofix engine with fix-applier and rule-fixer ([36efcec](https://github.com/markuplint/markuplint/commit/36efcecb17e2f4e0729390b1684e571c13c38a38))
22
+
23
+ ### BREAKING CHANGES
24
+
25
+ - **ml-core:** verify() now returns VerifyResult instead of
26
+ Violation[]. RuleSeed.fix() is removed in favor of inline fix
27
+ callbacks on report().
28
+
29
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
30
+
6
31
  # [5.0.0-alpha.1](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.0...v5.0.0-alpha.1) (2026-02-22)
7
32
 
8
33
  **Note:** Version bump only for package @markuplint/ml-core
@@ -109,15 +109,9 @@ await document.walkOn(
109
109
 
110
110
  `AccessibilityProperties` を返します([型ユーティリティ](./helpers.ja.md#型ユーティリティ)を参照)。
111
111
 
112
- ## `toString(fixed?)`
112
+ ## `toString()`
113
113
 
114
- トークンリストからソースコードを再構築します。
115
-
116
- - `fixed=false`(デフォルト):元の `raw` 文字列を返す
117
- - `fixed=true`:オフセット追跡による置換を適用する:
118
- 1. `getTokenList()` を取得する(`startOffset` でソート済み)
119
- 2. 各トークンに対して:`toString(true) !== raw` の場合、修正済みコンテンツを文字列にスプライスする
120
- 3. 正確な位置を維持するために累積オフセット差を追跡する
114
+ ドキュメントの元の `raw` ソース文字列を返します。修正は DOM ノードの変更とトークンリストの再構築を通じて適用されなくなりました。代わりに、レポートの修正コールバックが `RuleFixer` を通じて実行され、`TextEdit[]` を生成します。`FixApplier.applyFixes()` がすべての編集をソーステキストに直接適用します。
121
115
 
122
116
  ## `defaultView`
123
117
 
@@ -125,12 +119,11 @@ await document.walkOn(
125
119
 
126
120
  ## その他のメソッド
127
121
 
128
- | メソッド | シグネチャ | 説明 |
129
- | ---------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
130
- | `setRule` | `setRule(rule: MLRule \| null): void` | 現在評価中のルールを設定/クリアする |
131
- | `getTokenList` | `getTokenList(): ReadonlyArray<MLToken>` | ソース再構築用のすべてのトークン。オフセットでソート済み(キャッシュ)。ノードと閉じタグの両方を含む |
132
- | `searchNodeByLocation` | `searchNodeByLocation(line, col): MLNode \| null` | 1始まりのソース位置でノードを検索する |
133
- | `debugMap` | `debugMap(): string[]` | ドキュメントツリー構造のデバッグ出力 |
122
+ | メソッド | シグネチャ | 説明 |
123
+ | ---------------------- | ------------------------------------------------- | ------------------------------------- |
124
+ | `setRule` | `setRule(rule: MLRule \| null): void` | 現在評価中のルールを設定/クリアする |
125
+ | `searchNodeByLocation` | `searchNodeByLocation(line, col): MLNode \| null` | 1始まりのソース位置でノードを検索する |
126
+ | `debugMap` | `debugMap(): string[]` | ドキュメントツリー構造のデバッグ出力 |
134
127
 
135
128
  ## endTag モード
136
129
 
@@ -109,15 +109,9 @@ Computes ARIA accessibility properties for a node.
109
109
 
110
110
  Returns `AccessibilityProperties` (see [Type Utilities](./helpers.md#type-utilities)).
111
111
 
112
- ## `toString(fixed?)`
112
+ ## `toString()`
113
113
 
114
- Reconstructs the source code from the token list.
115
-
116
- - `fixed=false` (default): Returns the original `raw` string
117
- - `fixed=true`: Applies offset-tracked replacement:
118
- 1. Get `getTokenList()` (sorted by `startOffset`)
119
- 2. For each token: if `toString(true) !== raw`, splice the fixed content into the string
120
- 3. Track cumulative offset differences to maintain correct positions
114
+ Returns the original `raw` source string of the document. Fixes are no longer applied through DOM node mutation and token-list reconstruction. Instead, fix callbacks on reports are executed via `RuleFixer` to produce `TextEdit[]`. `FixApplier.applyFixes()` applies all edits to the source text.
121
115
 
122
116
  ## `defaultView`
123
117
 
@@ -125,12 +119,11 @@ Returns a mock window object providing `getComputedStyle()` that returns an obje
125
119
 
126
120
  ## Other Methods
127
121
 
128
- | Method | Signature | Description |
129
- | ---------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
130
- | `setRule` | `setRule(rule: MLRule \| null): void` | Set/clear the current rule being evaluated |
131
- | `getTokenList` | `getTokenList(): ReadonlyArray<MLToken>` | All tokens for source reconstruction, sorted by offset (cached). Includes both nodes and close tags. |
132
- | `searchNodeByLocation` | `searchNodeByLocation(line, col): MLNode \| null` | Find node at a 1-based source position |
133
- | `debugMap` | `debugMap(): string[]` | Debug output of document tree structure |
122
+ | Method | Signature | Description |
123
+ | ---------------------- | ------------------------------------------------- | ------------------------------------------ |
124
+ | `setRule` | `setRule(rule: MLRule \| null): void` | Set/clear the current rule being evaluated |
125
+ | `searchNodeByLocation` | `searchNodeByLocation(line, col): MLNode \| null` | Find node at a 1-based source position |
126
+ | `debugMap` | `debugMap(): string[]` | Debug output of document tree structure |
134
127
 
135
128
  ## endTag Modes
136
129
 
@@ -8,20 +8,18 @@ HTML/SVG/MathML の要素ノードです。`MLParentNode` を継承し、`Elemen
8
8
 
9
9
  要素には異なる目的に使用される複数の名前関連プロパティがあります:
10
10
 
11
- | プロパティ | HTML `<DIV>` | SVG `<foreignObject>` | Pretender(`MyButton` → `button`) |
12
- | --------------- | --------------------------- | --------------------- | ---------------------------------- |
13
- | `localName` | `"div"` | `"foreignObject"` | `"button"` |
14
- | `nodeName` | `"DIV"` | `"foreignObject"` | `"BUTTON"` |
15
- | `rawName` | `"DIV"` | `"foreignObject"` | `"MyButton"` |
16
- | `fixedNodeName` | `"DIV"`(修正後は `"div"`) | `"foreignObject"` | `"MyButton"` |
17
- | `tagName` | `"DIV"` | `"foreignObject"` | `"BUTTON"` |
11
+ | プロパティ | HTML `<DIV>` | SVG `<foreignObject>` | Pretender(`MyButton` → `button`) |
12
+ | ----------- | ------------ | --------------------- | ---------------------------------- |
13
+ | `localName` | `"div"` | `"foreignObject"` | `"button"` |
14
+ | `nodeName` | `"DIV"` | `"foreignObject"` | `"BUTTON"` |
15
+ | `rawName` | `"DIV"` | `"foreignObject"` | `"MyButton"` |
16
+ | `tagName` | `"DIV"` | `"foreignObject"` | `"BUTTON"` |
18
17
 
19
18
  **ルール:**
20
19
 
21
20
  - **`localName`**: HTML 要素 → 小文字化。外部要素または非 `'html'` の elementType → そのまま。pretender コンテキスト → pretender の `localName`。`tagNameCaseSensitive` が `true` の場合 → 小文字化しない。
22
21
  - **`nodeName`**: HTML 要素 → 大文字化(DOM 慣例)。外部要素または非 `'html'` の elementType → AST からそのまま。pretender コンテキスト → pretender の `nodeName`。
23
22
  - **`rawName`**: 常に元の AST `nodeName`。正規化なし、pretender の影響なし。
24
- - **`fixedNodeName`**: 初期値は `rawName`。リント修正がタグ名を変更した場合に `fixNodeName(name)` で更新される。
25
23
  - **`tagName`**: `nodeName` と同じ(pretender コンテキストに従う)。
26
24
 
27
25
  ## 要素型の判定
@@ -98,7 +96,6 @@ Pretender システムのアーキテクチャ、初期化フロー、プロパ
98
96
 
99
97
  | メソッド | シグネチャ | 説明 |
100
98
  | ------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------- |
101
- | `fixNodeName` | `fixNodeName(name: string): void` | リント自動修正用に `fixedNodeName` を更新する |
102
99
  | `getAccessibleName` | `getAccessibleName(version: ARIAVersion): string` | `getAccname()` を通じてアクセシブル名を計算する |
103
100
  | `toNormalizeString` | `toNormalizeString(): string` | 比較用の正規化された表現を返す(キャッシュ済み)。子要素と属性を再帰的に正規化する |
104
101
  | `nextElementSibling` | `get nextElementSibling: MLElement \| null` | 次の兄弟要素 |
@@ -134,7 +131,7 @@ Pretender システムのアーキテクチャ、初期化フロー、プロパ
134
131
 
135
132
  - 対応するソーストークンを持たない
136
133
  - `prevToken` によってスキップされる(有効なオフセットチェーンを維持するため)
137
- - `toString(fixed)` から `raw` を返す(ソースに修正すべきものがないため修正は適用されない)
134
+ - `toString()` から `raw` を返す(ソースに対応するものがないため)
138
135
  - `getChildElementsAndTextNodeWithoutWhitespaces()` によってフラット化される
139
136
 
140
137
  ## 閉じタグ
@@ -143,21 +140,9 @@ Pretender システムのアーキテクチャ、初期化フロー、プロパ
143
140
  | ---------- | --------------------------- | ------------------------------------------------------------------------------------ |
144
141
  | `closeTag` | `MLElementCloseTag \| null` | ペアの閉じタグ。void 要素、自己閉じ要素、または `endTag === 'never'` の場合は `null` |
145
142
 
146
- ## `toString(fixed?)`
143
+ ## `toString()`
147
144
 
148
- 修正を適用して要素のソース文字列を再構築します。
149
-
150
- - `fixed=false` または pretender/省略要素/`#` プレフィックスの nodeName → `raw` を返す
151
- - `fixed=true`:
152
- 1. 置換可能なノードのリストを構築する:`[tagOpenChar + fixedNodeName, ...overriddenCommentNodes, ...attributes]`
153
- 2. 各ノードに対して、正しいオフセットで `node.toString(true)` をスプライスする
154
- 3. 正確な位置決めのために累積オフセット差を追跡する
155
-
156
- ```
157
- Original: <DIV class="foo" >
158
- Fixed: <div class="foo" >
159
- ^^^^ (fixedNodeName が "DIV" から "div" に変更)
160
- ```
145
+ 要素の生のソース文字列を返します。このメソッドはパースされたドキュメントに記述されたままの元の `raw` ソーステキストを単純に返します。修正は DOM ノードの変更を通じて適用されなくなりました。代わりに、修正操作は `RuleFixer` を通じて `TextEdit[]` を生成し、`FixApplier.applyFixes()` がすべての編集をソーステキストに直接適用します。
161
146
 
162
147
  ## その他のプロパティ
163
148