mostajs-orm-validator 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 +90 -0
- package/llms.txt +66 -0
- package/package.json +105 -0
- package/src/extension.ts +365 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @mostajs/orm — ORMConceptValidator (VSCode Extension)
|
|
2
|
+
|
|
3
|
+
> **v0.1.0** — Plug-in VSCode pour le [`ORMConceptValidator`](https://www.npmjs.com/package/@mostajs/orm) de `@mostajs/orm`. Détecte 18 anomalies conceptuelles dans vos `EntitySchema` avec squiggles inline et quick-fix.
|
|
4
|
+
|
|
5
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Depuis le Marketplace *(quand publié)*
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
ext install apolocine.mostajs-orm-validator
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Depuis source *(développement)*
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/apolocine/mosta-orm-vscode
|
|
19
|
+
cd mosta-orm-vscode
|
|
20
|
+
npm install
|
|
21
|
+
npm run compile
|
|
22
|
+
# Ouvrir le dossier dans VSCode, F5 pour lancer un Extension Development Host
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Pré-requis
|
|
26
|
+
|
|
27
|
+
Le workspace doit avoir :
|
|
28
|
+
- `@mostajs/orm@^1.15.0` *(le validator CLI doit être accessible via `npx mostajs-orm-validator`)*
|
|
29
|
+
- Un dossier de schémas *(default `./schemas`, configurable)*
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Paramètres dans VSCode *(File > Preferences > Settings, chercher "mostajs orm")* :
|
|
34
|
+
|
|
35
|
+
| Paramètre | Default | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `mostajsOrmValidator.enabled` | `true` | Active/désactive l'extension pour le workspace |
|
|
38
|
+
| `mostajsOrmValidator.schemasDir` | `./schemas` | Dossier des fichiers `*.schema.ts` |
|
|
39
|
+
| `mostajsOrmValidator.sourceRoot` | `./lib` | Dossier source pour règles cross-file *(R005, R007, R008, R011, R012, R014, R015)* |
|
|
40
|
+
| `mostajsOrmValidator.ignore` | `[]` | Règles à ignorer *(ex: `["R015", "R017"]`)* |
|
|
41
|
+
| `mostajsOrmValidator.runOnSave` | `true` | Relancer à chaque save d'un `*.schema.ts` |
|
|
42
|
+
|
|
43
|
+
## Commandes *(Ctrl+Shift+P)*
|
|
44
|
+
|
|
45
|
+
| Commande | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| **ORMValidator: Validate current workspace** | Lance le validator sur tout le workspace |
|
|
48
|
+
| **ORMValidator: Apply all auto-fixes** | Applique `--fix` pour toutes les règles fixables *(V3-A V2)* |
|
|
49
|
+
| **ORMValidator: Rollback last --fix (restore .bak)** | Restaure les `.bak` du dernier `--fix` |
|
|
50
|
+
|
|
51
|
+
## Fonctionnement
|
|
52
|
+
|
|
53
|
+
À l'ouverture d'un fichier TypeScript, l'extension :
|
|
54
|
+
1. Lance `npx mostajs-orm-validator <schemasDir> --src <sourceRoot> --format json`
|
|
55
|
+
2. Parse les findings JSON
|
|
56
|
+
3. Affiche chaque finding comme un **Diagnostic VSCode** *(squiggle inline)* :
|
|
57
|
+
- 🔴 `error` → squiggle rouge
|
|
58
|
+
- 🟡 `warning` → squiggle jaune
|
|
59
|
+
- ℹ️ `info` → squiggle bleu
|
|
60
|
+
- 💡 `hint` → soulignement gris
|
|
61
|
+
4. Le code de la règle *(ex: R001-EMPTY-RELATIONS)* apparaît dans le tooltip + dans la barre Problems
|
|
62
|
+
5. Sur sauvegarde *(si `runOnSave: true`)*, relance la validation
|
|
63
|
+
|
|
64
|
+
## Quick-fix V3-C V1 *(limité)*
|
|
65
|
+
|
|
66
|
+
V0.1.0 propose des suggestions textuelles dans les tooltips. Les quick-fix
|
|
67
|
+
automatiques *(via VSCode Code Actions)* arrivent en V3-C V2 — pour l'instant
|
|
68
|
+
utiliser le CLI :
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx mostajs-orm-validator ./schemas --src ./lib --fix-dry-run # preview
|
|
72
|
+
npx mostajs-orm-validator ./schemas --src ./lib --fix # apply
|
|
73
|
+
npx mostajs-orm-validator ./schemas --rollback-fix # undo
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Limites V0.1.0
|
|
77
|
+
|
|
78
|
+
- Quick-fix VSCode Code Action : non implémenté *(V3-C V2)* — utiliser CLI pour appliquer
|
|
79
|
+
- LSP server dédié : pas encore *(V3-C V3, performance optimale)* — utilise le CLI via spawn
|
|
80
|
+
- Marketplace publish : pas encore *(décision utilisateur — nécessite PAT Azure DevOps)*
|
|
81
|
+
|
|
82
|
+
## Roadmap
|
|
83
|
+
|
|
84
|
+
- **V3-C V2** : LSP server natif *(perf, sans spawn CLI)* + Code Actions VSCode pour les quick-fix
|
|
85
|
+
- **V3-C V3** : Publish sur VSCode Marketplace + JetBrains Marketplace *(IntelliJ/WebStorm)*
|
|
86
|
+
- **V4** : Intégration GitHub Actions / preview dans PR
|
|
87
|
+
|
|
88
|
+
## Licence
|
|
89
|
+
|
|
90
|
+
AGPL-3.0-or-later
|
package/llms.txt
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# mostajs-orm-validator (extension VSCode) — fiche LLM
|
|
2
|
+
> Linter algorithmique des schémas @mostajs/orm — 18 anomalies conceptuelles avec squiggles + quick-fix.
|
|
3
|
+
|
|
4
|
+
- Version: 0.2.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Chemin: mostajs/mosta-orm-vscode · Statut audit: partiel (src/ + manifest VSCode ; out/ compilé)
|
|
6
|
+
|
|
7
|
+
## RÔLE
|
|
8
|
+
Extension Visual Studio Code (publisher `apolocine`). Analyse en continu les fichiers
|
|
9
|
+
`*.schema.ts` d'un projet @mostajs/orm et signale 18 anomalies conceptuelles (règles
|
|
10
|
+
R001-R018) directement dans l'éditeur, avec quick-fix pour les règles auto-corrigibles.
|
|
11
|
+
Importe @mostajs/orm/validator en-process (plus de spawn CLI), validation à la sauvegarde
|
|
12
|
+
(debounced) et au démarrage. Ce n'est pas un paquet npm importable mais une extension VSIX.
|
|
13
|
+
|
|
14
|
+
## INSTALLATION
|
|
15
|
+
Installer le `.vsix` (commande `vsce package` puis VSCode → Install from VSIX),
|
|
16
|
+
ou publier via `vsce publish`. Requiert VSCode ^1.80.0.
|
|
17
|
+
|
|
18
|
+
## EXPORTS
|
|
19
|
+
Aucun export de bibliothèque. L'extension expose des commandes et des réglages VSCode.
|
|
20
|
+
Point d'entrée : `./out/extension.js` — fonction `activate(context)`.
|
|
21
|
+
Activation : onLanguage:typescript, onLanguage:typescriptreact.
|
|
22
|
+
|
|
23
|
+
## API — COMMANDES VSCODE
|
|
24
|
+
- `mostajsOrmValidator.validate` — « ORMValidator: Validate current workspace »
|
|
25
|
+
- `mostajsOrmValidator.fixAll` — « ORMValidator: Apply all auto-fixes »
|
|
26
|
+
- `mostajsOrmValidator.fixOne` — « ORMValidator: Apply this fix » (quick-fix ciblé)
|
|
27
|
+
- `mostajsOrmValidator.rollback` — « ORMValidator: Rollback last --fix (restore .bak) »
|
|
28
|
+
|
|
29
|
+
## RÉGLAGES (settings.json)
|
|
30
|
+
- `mostajsOrmValidator.enabled` (boolean, def true) — activer le validator
|
|
31
|
+
- `mostajsOrmValidator.schemasDir` (string, def `./schemas`) — dossier des `*.schema.ts`
|
|
32
|
+
- `mostajsOrmValidator.sourceRoot` (string, def `./lib`) — racine des sources ;
|
|
33
|
+
active les règles cross-file R005, R007, R008, R011, R012, R014, R015
|
|
34
|
+
- `mostajsOrmValidator.ignore` (string[], def []) — règles à ignorer, ex `['R015','R017']`
|
|
35
|
+
- `mostajsOrmValidator.runOnSave` (boolean, def true) — revalider à chaque sauvegarde
|
|
36
|
+
|
|
37
|
+
## TYPES CLÉS
|
|
38
|
+
Sans objet — extension VSCode, pas de surface de types publique.
|
|
39
|
+
|
|
40
|
+
## PATTERN
|
|
41
|
+
```jsonc
|
|
42
|
+
// .vscode/settings.json du projet @mostajs/orm
|
|
43
|
+
{
|
|
44
|
+
"mostajsOrmValidator.schemasDir": "./src/schemas",
|
|
45
|
+
"mostajsOrmValidator.sourceRoot": "./src",
|
|
46
|
+
"mostajsOrmValidator.ignore": ["R015"]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
Ouvrir un fichier `*.schema.ts` : les findings apparaissent en squiggles ;
|
|
50
|
+
Ctrl+. propose les quick-fix sur les règles fixables.
|
|
51
|
+
|
|
52
|
+
## DÉPEND DE
|
|
53
|
+
- @mostajs/orm (dependency — utilise le sous-chemin `@mostajs/orm/validator` en-process)
|
|
54
|
+
- jiti (chargement des modules schéma TypeScript à la volée)
|
|
55
|
+
|
|
56
|
+
## PIÈGES
|
|
57
|
+
- Les règles cross-file (R005, R007, R008, R011, R012, R014, R015) ne s'activent que si
|
|
58
|
+
`sourceRoot` est correctement renseigné — sinon elles sont silencieusement inactives.
|
|
59
|
+
- `rollback` restaure les `.bak` créés par le dernier `--fix` ; idempotent (no-op sans `.bak`).
|
|
60
|
+
- Validation déclenchée uniquement sur fichiers `.ts`/`.tsx` et si `runOnSave` est actif
|
|
61
|
+
(debounce 300 ms).
|
|
62
|
+
- Le linter partage le moteur de @mostajs/orm/validator — version des règles liée à la
|
|
63
|
+
version de @mostajs/orm installée.
|
|
64
|
+
|
|
65
|
+
## RÉFÉRENCES
|
|
66
|
+
- README.md · (règles documentées dans mosta-orm/docs/ORMConceptValidator.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mostajs-orm-validator",
|
|
3
|
+
"displayName": "@mostajs/orm — ORMConceptValidator",
|
|
4
|
+
"description": "Algorithmic linter for @mostajs/orm schemas. Detects 18 conceptual anomalies with inline squiggles + quick-fix.",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"license": "AGPL-3.0-or-later",
|
|
8
|
+
"publisher": "apolocine",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/apolocine/mosta-orm-vscode"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"vscode": "^1.80.0"
|
|
15
|
+
},
|
|
16
|
+
"categories": [
|
|
17
|
+
"Linters",
|
|
18
|
+
"Other"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"orm",
|
|
22
|
+
"validator",
|
|
23
|
+
"linter",
|
|
24
|
+
"mostajs",
|
|
25
|
+
"schema"
|
|
26
|
+
],
|
|
27
|
+
"main": "./out/extension.js",
|
|
28
|
+
"activationEvents": [
|
|
29
|
+
"onLanguage:typescript",
|
|
30
|
+
"onLanguage:typescriptreact"
|
|
31
|
+
],
|
|
32
|
+
"contributes": {
|
|
33
|
+
"configuration": {
|
|
34
|
+
"title": "@mostajs/orm — ORMConceptValidator",
|
|
35
|
+
"properties": {
|
|
36
|
+
"mostajsOrmValidator.enabled": {
|
|
37
|
+
"type": "boolean",
|
|
38
|
+
"default": true,
|
|
39
|
+
"description": "Activer/désactiver le validator pour ce workspace."
|
|
40
|
+
},
|
|
41
|
+
"mostajsOrmValidator.schemasDir": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"default": "./schemas",
|
|
44
|
+
"description": "Chemin relatif vers le dossier contenant les fichiers *.schema.ts."
|
|
45
|
+
},
|
|
46
|
+
"mostajsOrmValidator.sourceRoot": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"default": "./lib",
|
|
49
|
+
"description": "Chemin relatif vers les sources (active les règles cross-file R005, R007, R008, R011, R012, R014, R015)."
|
|
50
|
+
},
|
|
51
|
+
"mostajsOrmValidator.ignore": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"default": [],
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
"description": "Liste des règles à ignorer (ex: ['R015', 'R017'])."
|
|
58
|
+
},
|
|
59
|
+
"mostajsOrmValidator.runOnSave": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"default": true,
|
|
62
|
+
"description": "Relancer la validation à chaque sauvegarde d'un schema."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"commands": [
|
|
67
|
+
{
|
|
68
|
+
"command": "mostajsOrmValidator.validate",
|
|
69
|
+
"title": "ORMValidator: Validate current workspace",
|
|
70
|
+
"category": "@mostajs/orm"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"command": "mostajsOrmValidator.fixAll",
|
|
74
|
+
"title": "ORMValidator: Apply all auto-fixes",
|
|
75
|
+
"category": "@mostajs/orm"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"command": "mostajsOrmValidator.fixOne",
|
|
79
|
+
"title": "ORMValidator: Apply this fix",
|
|
80
|
+
"category": "@mostajs/orm"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"command": "mostajsOrmValidator.rollback",
|
|
84
|
+
"title": "ORMValidator: Rollback last --fix (restore .bak)",
|
|
85
|
+
"category": "@mostajs/orm"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@mostajs/orm": "^1.16.0",
|
|
91
|
+
"jiti": "^2.7.0"
|
|
92
|
+
},
|
|
93
|
+
"devDependencies": {
|
|
94
|
+
"@types/vscode": "^1.80.0",
|
|
95
|
+
"@types/node": "^22.0.0",
|
|
96
|
+
"@vscode/vsce": "^3.0.0",
|
|
97
|
+
"typescript": "^5.6.0"
|
|
98
|
+
},
|
|
99
|
+
"scripts": {
|
|
100
|
+
"compile": "tsc",
|
|
101
|
+
"watch": "tsc --watch",
|
|
102
|
+
"vscode:prepublish": "npm run compile",
|
|
103
|
+
"package": "vsce package"
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// @mostajs/orm — VSCode extension v0.2.0 (V3-C V2)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
//
|
|
4
|
+
// V3-C V2 — Améliorations vs V0.1.0 :
|
|
5
|
+
// - Import direct @mostajs/orm/validator (plus de spawn CLI)
|
|
6
|
+
// - Code Actions (quick-fix) pour les findings fixables
|
|
7
|
+
// - Hot reload sur change (debounced)
|
|
8
|
+
// - Performance : in-process, partagé entre fichiers
|
|
9
|
+
|
|
10
|
+
import * as vscode from 'vscode'
|
|
11
|
+
import * as path from 'node:path'
|
|
12
|
+
import * as fs from 'node:fs'
|
|
13
|
+
|
|
14
|
+
let diagnosticCollection: vscode.DiagnosticCollection
|
|
15
|
+
let outputChannel: vscode.OutputChannel
|
|
16
|
+
let lastReport: any = null
|
|
17
|
+
let debounceHandle: NodeJS.Timeout | null = null
|
|
18
|
+
|
|
19
|
+
const DEBOUNCE_MS = 300
|
|
20
|
+
|
|
21
|
+
export async function activate(context: vscode.ExtensionContext) {
|
|
22
|
+
outputChannel = vscode.window.createOutputChannel('ORMConceptValidator')
|
|
23
|
+
diagnosticCollection = vscode.languages.createDiagnosticCollection('mostajsOrmValidator')
|
|
24
|
+
context.subscriptions.push(diagnosticCollection, outputChannel)
|
|
25
|
+
|
|
26
|
+
// Commands
|
|
27
|
+
context.subscriptions.push(
|
|
28
|
+
vscode.commands.registerCommand('mostajsOrmValidator.validate', runValidation),
|
|
29
|
+
vscode.commands.registerCommand('mostajsOrmValidator.fixAll', runFixAll),
|
|
30
|
+
vscode.commands.registerCommand('mostajsOrmValidator.fixOne', runFixOne),
|
|
31
|
+
vscode.commands.registerCommand('mostajsOrmValidator.rollback', runRollback),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Trigger : on save (debounced)
|
|
35
|
+
context.subscriptions.push(
|
|
36
|
+
vscode.workspace.onDidSaveTextDocument((doc) => {
|
|
37
|
+
if (!isEnabled()) return
|
|
38
|
+
if (!doc.fileName.endsWith('.ts') && !doc.fileName.endsWith('.tsx')) return
|
|
39
|
+
const cfg = vscode.workspace.getConfiguration('mostajsOrmValidator')
|
|
40
|
+
if (!cfg.get<boolean>('runOnSave', true)) return
|
|
41
|
+
scheduleValidation()
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Code Action provider — propose quick-fix sur les findings fixables
|
|
46
|
+
context.subscriptions.push(
|
|
47
|
+
vscode.languages.registerCodeActionsProvider(
|
|
48
|
+
{ language: 'typescript' },
|
|
49
|
+
new ValidatorCodeActionProvider(),
|
|
50
|
+
{ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] },
|
|
51
|
+
),
|
|
52
|
+
vscode.languages.registerCodeActionsProvider(
|
|
53
|
+
{ language: 'typescriptreact' },
|
|
54
|
+
new ValidatorCodeActionProvider(),
|
|
55
|
+
{ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] },
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// Validation initiale au démarrage
|
|
60
|
+
if (isEnabled() && hasSchemasDir()) {
|
|
61
|
+
await runValidation()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
outputChannel.appendLine('ORMConceptValidator V3-C V2 activated.')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function deactivate() {
|
|
68
|
+
if (debounceHandle) clearTimeout(debounceHandle)
|
|
69
|
+
diagnosticCollection?.dispose()
|
|
70
|
+
outputChannel?.dispose()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scheduleValidation() {
|
|
74
|
+
if (debounceHandle) clearTimeout(debounceHandle)
|
|
75
|
+
debounceHandle = setTimeout(() => { void runValidation() }, DEBOUNCE_MS)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Validation ─────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
async function runValidation() {
|
|
81
|
+
const workspace = vscode.workspace.workspaceFolders?.[0]
|
|
82
|
+
if (!workspace) return
|
|
83
|
+
|
|
84
|
+
const cfg = vscode.workspace.getConfiguration('mostajsOrmValidator')
|
|
85
|
+
const schemasDir = path.join(workspace.uri.fsPath, cfg.get<string>('schemasDir') ?? './schemas')
|
|
86
|
+
const sourceRoot = path.join(workspace.uri.fsPath, cfg.get<string>('sourceRoot') ?? './lib')
|
|
87
|
+
const ignore = cfg.get<string[]>('ignore') ?? []
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(schemasDir)) {
|
|
90
|
+
outputChannel.appendLine(`Skip : schemasDir introuvable (${schemasDir})`)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Import direct (in-process). Lazy load car @mostajs/orm/validator + ts-morph
|
|
96
|
+
// est lourd (~2 MB) — on ne le charge qu'à la 1ère validation.
|
|
97
|
+
const validator = await import('@mostajs/orm/validator')
|
|
98
|
+
const { createJiti } = await import('jiti')
|
|
99
|
+
|
|
100
|
+
// Charge les schemas via jiti (TS direct)
|
|
101
|
+
const jiti = createJiti(require.main?.filename ?? __filename, { interopDefault: true, fsCache: false })
|
|
102
|
+
const byName = new Map<string, any>()
|
|
103
|
+
walkSchemas(schemasDir, async (filePath) => {
|
|
104
|
+
try {
|
|
105
|
+
const mod: any = await jiti.import(filePath)
|
|
106
|
+
for (const exp of Object.values(mod)) {
|
|
107
|
+
if (isEntitySchema(exp)) {
|
|
108
|
+
const s = exp as any
|
|
109
|
+
if (!byName.has(s.name)) byName.set(s.name, s)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
outputChannel.appendLine(`Skip ${filePath}: ${(e as Error).message}`)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
const schemas = [...byName.values()]
|
|
117
|
+
if (schemas.length === 0) {
|
|
118
|
+
outputChannel.appendLine(`Aucun schéma trouvé dans ${schemasDir}`)
|
|
119
|
+
diagnosticCollection.clear()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const report = await validator.validateSchemas(schemas, {
|
|
124
|
+
sourceRoot: fs.existsSync(sourceRoot) ? sourceRoot : undefined,
|
|
125
|
+
ignore,
|
|
126
|
+
})
|
|
127
|
+
lastReport = report
|
|
128
|
+
|
|
129
|
+
// Push diagnostics
|
|
130
|
+
diagnosticCollection.clear()
|
|
131
|
+
const findingsByFile = new Map<string, any[]>()
|
|
132
|
+
for (const f of report.findings) {
|
|
133
|
+
const file = locateFindingFile(f, schemasDir, workspace.uri.fsPath)
|
|
134
|
+
if (!file) continue
|
|
135
|
+
if (!findingsByFile.has(file)) findingsByFile.set(file, [])
|
|
136
|
+
findingsByFile.get(file)!.push(f)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [file, findings] of findingsByFile) {
|
|
140
|
+
const diagnostics: vscode.Diagnostic[] = []
|
|
141
|
+
for (const f of findings) {
|
|
142
|
+
const range = computeRange(file, f)
|
|
143
|
+
const diag = new vscode.Diagnostic(range, `[${f.ruleId}] ${f.message}`, severityToVSCode(f.severity))
|
|
144
|
+
diag.source = '@mostajs/orm-validator'
|
|
145
|
+
diag.code = f.ruleId
|
|
146
|
+
// Stockage payload pour Code Action récupérer ensuite
|
|
147
|
+
;(diag as any).fixableData = f
|
|
148
|
+
diagnostics.push(diag)
|
|
149
|
+
}
|
|
150
|
+
diagnosticCollection.set(vscode.Uri.file(file), diagnostics)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
outputChannel.appendLine(`✓ ${report.findings.length} findings (${report.countBySeverity.warning} warnings, ${report.countBySeverity.info} infos, ${report.countBySeverity.hint} hints) sur ${schemas.length} schémas — ${report.durationMs}ms`)
|
|
154
|
+
} catch (e) {
|
|
155
|
+
outputChannel.appendLine(`Error: ${(e as Error).stack ?? (e as Error).message}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Code Actions provider (quick-fix) ──────────────────────────
|
|
160
|
+
|
|
161
|
+
class ValidatorCodeActionProvider implements vscode.CodeActionProvider {
|
|
162
|
+
provideCodeActions(
|
|
163
|
+
document: vscode.TextDocument,
|
|
164
|
+
range: vscode.Range,
|
|
165
|
+
context: vscode.CodeActionContext,
|
|
166
|
+
): vscode.CodeAction[] {
|
|
167
|
+
const actions: vscode.CodeAction[] = []
|
|
168
|
+
for (const diag of context.diagnostics) {
|
|
169
|
+
if (diag.source !== '@mostajs/orm-validator') continue
|
|
170
|
+
const finding = (diag as any).fixableData
|
|
171
|
+
if (!finding?.fixable) continue
|
|
172
|
+
const action = new vscode.CodeAction(
|
|
173
|
+
`⚙ Auto-fix [${finding.ruleId}] ${finding.location.schema}${finding.location.field ? '.' + finding.location.field : ''}`,
|
|
174
|
+
vscode.CodeActionKind.QuickFix,
|
|
175
|
+
)
|
|
176
|
+
action.command = {
|
|
177
|
+
title: 'Auto-fix',
|
|
178
|
+
command: 'mostajsOrmValidator.fixOne',
|
|
179
|
+
arguments: [finding],
|
|
180
|
+
}
|
|
181
|
+
action.diagnostics = [diag]
|
|
182
|
+
action.isPreferred = true
|
|
183
|
+
actions.push(action)
|
|
184
|
+
}
|
|
185
|
+
return actions
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runFixOne(finding: any) {
|
|
190
|
+
if (!lastReport) {
|
|
191
|
+
vscode.window.showWarningMessage('Aucun rapport en mémoire — lance "Validate" d\'abord.')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
await applyFixesFiltered([finding])
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function runFixAll() {
|
|
198
|
+
if (!lastReport) {
|
|
199
|
+
vscode.window.showWarningMessage('Aucun rapport en mémoire — lance "Validate" d\'abord.')
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const fixable = lastReport.findings.filter((f: any) => f.fixable)
|
|
203
|
+
if (fixable.length === 0) {
|
|
204
|
+
vscode.window.showInformationMessage('Aucun finding auto-fixable dans le rapport.')
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const choice = await vscode.window.showWarningMessage(
|
|
208
|
+
`Appliquer ${fixable.length} auto-fix ? (backup .bak créé)`,
|
|
209
|
+
{ modal: true },
|
|
210
|
+
'Apply', 'Dry-run',
|
|
211
|
+
)
|
|
212
|
+
if (!choice) return
|
|
213
|
+
await applyFixesFiltered(fixable, choice === 'Dry-run')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function applyFixesFiltered(findings: any[], dryRun = false) {
|
|
217
|
+
const workspace = vscode.workspace.workspaceFolders?.[0]
|
|
218
|
+
if (!workspace) return
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const validator = await import('@mostajs/orm/validator')
|
|
222
|
+
const cfg = vscode.workspace.getConfiguration('mostajsOrmValidator')
|
|
223
|
+
const sourceRoot = path.join(workspace.uri.fsPath, cfg.get<string>('schemasDir') ?? './schemas')
|
|
224
|
+
|
|
225
|
+
// Construit un mini-rapport ciblé
|
|
226
|
+
const targetedReport = { ...lastReport, findings }
|
|
227
|
+
const results = await validator.applyFixes(targetedReport as any, {
|
|
228
|
+
sourceRoot,
|
|
229
|
+
dryRun,
|
|
230
|
+
backup: true,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const applied = results.filter(r => r.applied).length
|
|
234
|
+
const skipped = results.filter(r => !r.applied).length
|
|
235
|
+
const msg = `${dryRun ? 'Dry-run' : 'Apply'} : ${applied} fix${applied > 1 ? 'es' : ''} ${dryRun ? 'préviewés' : 'appliqués'}, ${skipped} skippés.`
|
|
236
|
+
outputChannel.appendLine(msg)
|
|
237
|
+
for (const r of results) {
|
|
238
|
+
outputChannel.appendLine(` ${r.applied ? '✓' : '·'} ${r.ruleId} ${r.schema}${r.field ? '.' + r.field : ''} ${r.reason ?? ''}`)
|
|
239
|
+
if (r.diff && dryRun) outputChannel.appendLine(r.diff)
|
|
240
|
+
}
|
|
241
|
+
vscode.window.showInformationMessage(msg + (dryRun ? '' : ' (.bak créés)'))
|
|
242
|
+
|
|
243
|
+
if (!dryRun) {
|
|
244
|
+
// Relance validation après fix pour rafraîchir les diagnostics
|
|
245
|
+
setTimeout(() => { void runValidation() }, 200)
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
outputChannel.appendLine(`Fix error: ${(e as Error).message}`)
|
|
249
|
+
vscode.window.showErrorMessage(`Auto-fix échec: ${(e as Error).message}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function runRollback() {
|
|
254
|
+
const workspace = vscode.workspace.workspaceFolders?.[0]
|
|
255
|
+
if (!workspace) return
|
|
256
|
+
try {
|
|
257
|
+
const validator = await import('@mostajs/orm/validator')
|
|
258
|
+
const cfg = vscode.workspace.getConfiguration('mostajsOrmValidator')
|
|
259
|
+
const schemasDir = path.join(workspace.uri.fsPath, cfg.get<string>('schemasDir') ?? './schemas')
|
|
260
|
+
const results = validator.rollbackFixes(schemasDir)
|
|
261
|
+
const ok = results.filter((r) => r.restored).length
|
|
262
|
+
const fail = results.filter((r) => !r.restored).length
|
|
263
|
+
outputChannel.appendLine(`Rollback: ${ok} restored, ${fail} failed`)
|
|
264
|
+
for (const r of results) {
|
|
265
|
+
outputChannel.appendLine(` ${r.restored ? '✓' : '✗'} ${r.file}${r.reason ? ' — ' + r.reason : ''}`)
|
|
266
|
+
}
|
|
267
|
+
vscode.window.showInformationMessage(`Rollback : ${ok} fichier${ok > 1 ? 's' : ''} restauré${ok > 1 ? 's' : ''}.`)
|
|
268
|
+
setTimeout(() => { void runValidation() }, 200)
|
|
269
|
+
} catch (e) {
|
|
270
|
+
vscode.window.showErrorMessage(`Rollback échec: ${(e as Error).message}`)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function isEnabled(): boolean {
|
|
277
|
+
return vscode.workspace.getConfiguration('mostajsOrmValidator').get<boolean>('enabled', true)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hasSchemasDir(): boolean {
|
|
281
|
+
const workspace = vscode.workspace.workspaceFolders?.[0]
|
|
282
|
+
if (!workspace) return false
|
|
283
|
+
const cfg = vscode.workspace.getConfiguration('mostajsOrmValidator')
|
|
284
|
+
return fs.existsSync(path.join(workspace.uri.fsPath, cfg.get<string>('schemasDir') ?? './schemas'))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function walkSchemas(dir: string, cb: (file: string) => Promise<void> | void) {
|
|
288
|
+
let entries: string[]
|
|
289
|
+
try { entries = fs.readdirSync(dir) } catch { return }
|
|
290
|
+
for (const e of entries) {
|
|
291
|
+
if (e === 'node_modules' || e === 'dist' || e === '.next' || e === '.git') continue
|
|
292
|
+
const full = path.join(dir, e)
|
|
293
|
+
let st
|
|
294
|
+
try { st = fs.statSync(full) } catch { continue }
|
|
295
|
+
if (st.isDirectory()) walkSchemas(full, cb)
|
|
296
|
+
else if ((full.endsWith('.ts') || full.endsWith('.tsx')) && !full.endsWith('.d.ts')) {
|
|
297
|
+
void cb(full)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isEntitySchema(x: any): boolean {
|
|
303
|
+
return x && typeof x === 'object'
|
|
304
|
+
&& typeof x.name === 'string'
|
|
305
|
+
&& typeof x.collection === 'string'
|
|
306
|
+
&& x.fields && typeof x.fields === 'object'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function locateFindingFile(f: any, schemasDir: string, workspaceRoot: string): string | null {
|
|
310
|
+
if (f.location?.file) {
|
|
311
|
+
const abs = path.isAbsolute(f.location.file)
|
|
312
|
+
? f.location.file
|
|
313
|
+
: path.resolve(workspaceRoot, f.location.file)
|
|
314
|
+
return fs.existsSync(abs) ? abs : null
|
|
315
|
+
}
|
|
316
|
+
if (f.location?.schema) {
|
|
317
|
+
// Cherche le fichier contenant export const <SchemaName>Schema
|
|
318
|
+
const re = new RegExp(`export\\s+const\\s+${escapeRegex(f.location.schema)}Schema\\s*[:=]`)
|
|
319
|
+
let found: string | null = null
|
|
320
|
+
walkSchemas(schemasDir, (file) => {
|
|
321
|
+
if (found) return
|
|
322
|
+
try {
|
|
323
|
+
if (re.test(fs.readFileSync(file, 'utf-8'))) found = file
|
|
324
|
+
} catch { /* skip */ }
|
|
325
|
+
})
|
|
326
|
+
return found
|
|
327
|
+
}
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function computeRange(file: string, finding: any): vscode.Range {
|
|
332
|
+
if (finding.location?.line) {
|
|
333
|
+
const line = (finding.location.line - 1) | 0
|
|
334
|
+
return new vscode.Range(line, 0, line, 999)
|
|
335
|
+
}
|
|
336
|
+
// Cherche la déclaration du field dans le fichier pour placer le squiggle
|
|
337
|
+
if (finding.location?.schema && finding.location?.field) {
|
|
338
|
+
try {
|
|
339
|
+
const content = fs.readFileSync(file, 'utf-8')
|
|
340
|
+
const re = new RegExp(`\\b${escapeRegex(finding.location.field)}\\s*:`, 'm')
|
|
341
|
+
const m = content.match(re)
|
|
342
|
+
if (m && m.index !== undefined) {
|
|
343
|
+
const before = content.slice(0, m.index)
|
|
344
|
+
const line = (before.match(/\n/g) ?? []).length
|
|
345
|
+
const col = m.index - before.lastIndexOf('\n') - 1
|
|
346
|
+
return new vscode.Range(line, col, line, col + finding.location.field.length)
|
|
347
|
+
}
|
|
348
|
+
} catch { /* fall through */ }
|
|
349
|
+
}
|
|
350
|
+
return new vscode.Range(0, 0, 0, 0)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function escapeRegex(s: string): string {
|
|
354
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function severityToVSCode(sev: string): vscode.DiagnosticSeverity {
|
|
358
|
+
switch (sev) {
|
|
359
|
+
case 'error': return vscode.DiagnosticSeverity.Error
|
|
360
|
+
case 'warning': return vscode.DiagnosticSeverity.Warning
|
|
361
|
+
case 'info': return vscode.DiagnosticSeverity.Information
|
|
362
|
+
case 'hint': return vscode.DiagnosticSeverity.Hint
|
|
363
|
+
default: return vscode.DiagnosticSeverity.Information
|
|
364
|
+
}
|
|
365
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "out",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "out", ".vscode-test"]
|
|
17
|
+
}
|