0.0.5-alpha.5.fix.1
This commit is contained in:
217
deltas/0.0.5/alpha.5.fix.1.md
Normal file
217
deltas/0.0.5/alpha.5.fix.1.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<!-- file: deltas/0.0.5/alpha.5.fix.1.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.0.5-alpha.5.fix.1 — canonicalisation réelle des références crate-root
|
||||||
|
|
||||||
|
## Baseline et version
|
||||||
|
|
||||||
|
Baseline attendue : commit opérateur complet de `0.0.5-alpha.5`, réalisé après application de `history/0.0.5/alpha.5.md` avec statut `FAIL`.
|
||||||
|
|
||||||
|
Version avant application :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.5-alpha.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Version après application :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.5-alpha.5.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cause du fix
|
||||||
|
|
||||||
|
La gate mécanique de `alpha.5` est passée, mais la revue fonctionnelle opérateur a identifié un blind spot dans `RUST-IMPORT-201` : la `version: 1` détecte `crate::module::Item` lorsque `crate::Item` existe, mais ne détecte pas un usage nu ou autrement non canonique de ce même item dans son module propriétaire ou un autre module.
|
||||||
|
|
||||||
|
Or le contrat recherché est bien :
|
||||||
|
|
||||||
|
```text
|
||||||
|
item partagé pub/pub(crate)
|
||||||
|
↓
|
||||||
|
façade crate-root
|
||||||
|
↓
|
||||||
|
toutes les références intra-crate détectées utilisent crate::Item
|
||||||
|
```
|
||||||
|
|
||||||
|
Le compilateur et Clippy ne possèdent pas cette convention. Une seule référence correcte à `crate::Item` peut rendre le réexport utilisé tout en laissant subsister d'autres `Item`, `self::Item`, `super::Item` ou chemins internes.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
REPLACE :
|
||||||
|
|
||||||
|
```text
|
||||||
|
VERSION
|
||||||
|
docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md
|
||||||
|
docs/rules/510-RUST_CARGO.md
|
||||||
|
docs/studies/001-KSP_GAMES_RECONCILIATION.md
|
||||||
|
docs/validation/003-RUST_EXPORT_COMPLETENESS.md
|
||||||
|
scripts/audit_rust_export_completeness.py
|
||||||
|
tests/test_rust_export_completeness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
ADD :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.0.5/alpha.5.fix.1.md
|
||||||
|
```
|
||||||
|
|
||||||
|
DELETE : aucun.
|
||||||
|
|
||||||
|
Le history `alpha.5` appartient au commit baseline et n'est pas inclus dans l'archive du présent delta.
|
||||||
|
|
||||||
|
## Décisions matérialisées
|
||||||
|
|
||||||
|
### RUST-IMPORT-201 couvre toute référence non canonique détectable
|
||||||
|
|
||||||
|
Pour un item possédant une identité crate-root, la seule forme intra-crate canonique est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crate::Item
|
||||||
|
```
|
||||||
|
|
||||||
|
Le diagnostic couvre désormais les formes suivantes lorsqu'elles désignent cet item partagé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Item
|
||||||
|
self::Item
|
||||||
|
super::Item
|
||||||
|
module::Item
|
||||||
|
crate::module::Item
|
||||||
|
```
|
||||||
|
|
||||||
|
La règle s'applique au module propriétaire lui-même : signatures, `impl`, constructeurs, appels et références de types doivent employer la façade crate-root lorsqu'ils portent l'item partagé.
|
||||||
|
|
||||||
|
Les déclarations qui créent l'item et les lignes de réexport qui construisent la façade sont exemptées. Les éléments strictement privés restent hors de cette règle ; leurs tests attachés peuvent utiliser `super::PrivateItem`.
|
||||||
|
|
||||||
|
### Targets Cargo distinctes
|
||||||
|
|
||||||
|
Le fix évite également de confondre bibliothèque et binaire d'un même package. `src/lib.rs`, `src/main.rs`, les bins standard et les targets explicites sont traités comme des crates compilées distinctes. Le graphe de modules de chaque target est suivi via `mod name;` et `#[path = "..."]`.
|
||||||
|
|
||||||
|
Cette correction est nécessaire pour ne pas qualifier à tort `owner_crate::Item` dans un binaire comme référence intra-crate de la bibliothèque du même package.
|
||||||
|
|
||||||
|
### Analyse lexicale bornée
|
||||||
|
|
||||||
|
L'audit reste read-only et standard library. Il ne prétend pas remplacer un parseur Rust :
|
||||||
|
|
||||||
|
- les commentaires et littéraux sont masqués ;
|
||||||
|
- les chemins qualifiés par une crate externe ne sont pas assimilés à l'item local homonyme ;
|
||||||
|
- un appel de méthode `.name()` n'est pas assimilé à une fonction libre locale de même nom ;
|
||||||
|
- les fonctions libres sont contrôlées dans les contextes d'appel directs ; leur usage comme valeur de premier ordre peut nécessiter une revue humaine.
|
||||||
|
|
||||||
|
### Réconciliation corrigée
|
||||||
|
|
||||||
|
En `--report-only --summary-only`, la `version: 2` observe sur les baselines de `alpha.1` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP 0.3.16
|
||||||
|
22 package(s)
|
||||||
|
1525 candidate(s)
|
||||||
|
RUST-IMPORT-201: 1525
|
||||||
|
|
||||||
|
Games 0.3.5
|
||||||
|
23 package(s)
|
||||||
|
181 candidate(s)
|
||||||
|
RUST-API-201: 2
|
||||||
|
RUST-IMPORT-201: 179
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces nombres sont des lignes candidates à revoir, pas des déclarations distinctes. Ils ne constituent pas une non-conformité rétroactive : KSP et Games ne sont pas encore gouvernés par ce PGR. Ils montrent pourquoi une future adoption doit commencer par un inventaire `--report-only` et une migration proportionnée.
|
||||||
|
|
||||||
|
## Validation opérateur
|
||||||
|
|
||||||
|
Après application du history `FAIL` de `alpha.5`, commit/push de cette tranche, puis application du présent fix :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test "$(cat VERSION)" = '0.0.5-alpha.5.fix.1'
|
||||||
|
test -f history/0.0.5/alpha.5.md
|
||||||
|
test -f deltas/0.0.5/alpha.5.fix.1.md
|
||||||
|
test ! -e history/0.0.5/alpha.5.fix.1.md
|
||||||
|
|
||||||
|
grep -A2 '^## Statut$' history/0.0.5/alpha.5.md | grep -Fx 'FAIL'
|
||||||
|
grep -Fx '# version: 2' scripts/audit_rust_export_completeness.py
|
||||||
|
grep -Fx '# version: 2' tests/test_rust_export_completeness.py
|
||||||
|
grep -Fx '<!-- version: 2 -->' docs/validation/003-RUST_EXPORT_COMPLETENESS.md
|
||||||
|
grep -Fx '<!-- version: 4 -->' docs/rules/510-RUST_CARGO.md
|
||||||
|
|
||||||
|
python3 -B -m unittest discover -s tests -p 'test_rust_export_completeness.py' -v
|
||||||
|
python3 -B -m unittest discover -s tests -v
|
||||||
|
|
||||||
|
python3 -B scripts/audit_markdown_headers.py \
|
||||||
|
README.md RULES.md ROADMAP.md CHANGELOG.md docs deltas history prompts
|
||||||
|
python3 -B scripts/audit_markdown_tables.py \
|
||||||
|
README.md RULES.md ROADMAP.md CHANGELOG.md docs deltas history prompts
|
||||||
|
python3 -B scripts/audit_markdown_links.py \
|
||||||
|
README.md RULES.md ROADMAP.md CHANGELOG.md docs deltas history prompts
|
||||||
|
|
||||||
|
python3 -B - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
history = Path('history/0.0.5/alpha.5.md').read_text(encoding='utf-8')
|
||||||
|
assert re.search(r'## Statut\s+FAIL\s', history)
|
||||||
|
|
||||||
|
script = Path('scripts/audit_rust_export_completeness.py').read_text(encoding='utf-8')
|
||||||
|
for phrase in (
|
||||||
|
'def target_roots(',
|
||||||
|
'def target_modules(',
|
||||||
|
'def _canonical_reference_candidates(',
|
||||||
|
'RUST-API-201',
|
||||||
|
'RUST-IMPORT-201',
|
||||||
|
'non-canonical same-crate reference',
|
||||||
|
):
|
||||||
|
assert phrase in script, phrase
|
||||||
|
|
||||||
|
validation = Path('docs/validation/003-RUST_EXPORT_COMPLETENESS.md').read_text(encoding='utf-8')
|
||||||
|
for phrase in (
|
||||||
|
'Item\nself::Item\nsuper::Item\nmodule::Item\ncrate::module::Item',
|
||||||
|
'dans le module qui déclare l\'item',
|
||||||
|
'1525 candidate(s)',
|
||||||
|
'181 candidate(s)',
|
||||||
|
'pas des déclarations distinctes',
|
||||||
|
):
|
||||||
|
assert phrase in validation, phrase
|
||||||
|
|
||||||
|
rules = Path('docs/rules/510-RUST_CARGO.md').read_text(encoding='utf-8')
|
||||||
|
assert 'toute référence dans la même crate MUST utiliser `crate::Item`' in rules
|
||||||
|
assert 'y compris depuis son module de déclaration' in rules
|
||||||
|
|
||||||
|
print('0.0.5 alpha.5.fix.1 canonical Rust path audit: clean (owner module, cross-module, attached tests, distinct Cargo targets)')
|
||||||
|
PY
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
|
||||||
|
git add VERSION \
|
||||||
|
docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md \
|
||||||
|
docs/rules/510-RUST_CARGO.md \
|
||||||
|
docs/studies/001-KSP_GAMES_RECONCILIATION.md \
|
||||||
|
docs/validation/003-RUST_EXPORT_COMPLETENESS.md \
|
||||||
|
scripts/audit_rust_export_completeness.py \
|
||||||
|
tests/test_rust_export_completeness.py \
|
||||||
|
deltas/0.0.5/alpha.5.fix.1.md
|
||||||
|
|
||||||
|
git diff --cached --check
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
La sortie Git attendue avant ajout du history du fix est exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
M VERSION
|
||||||
|
A deltas/0.0.5/alpha.5.fix.1.md
|
||||||
|
M docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md
|
||||||
|
M docs/rules/510-RUST_CARGO.md
|
||||||
|
M docs/studies/001-KSP_GAMES_RECONCILIATION.md
|
||||||
|
M docs/validation/003-RUST_EXPORT_COMPLETENESS.md
|
||||||
|
M scripts/audit_rust_export_completeness.py
|
||||||
|
M tests/test_rust_export_completeness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
### PASS
|
||||||
|
|
||||||
|
Créer/appliquer `history/0.0.5/alpha.5.fix.1.md`, committer/pousser `0.0.5-alpha.5.fix.1`, puis poursuivre à `0.0.5-alpha.6` pour le profil Tauri prévu par le plan.
|
||||||
|
|
||||||
|
### FAIL
|
||||||
|
|
||||||
|
Rester dans `0.0.5-alpha.5.fix.M`. Aucun travail Tauri ne doit être absorbé tant que le contrat de façade Rust n'est pas correctement fermé.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md -->
|
<!-- file: docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Plan 0.0.5 — réconciliation réelle avec KSP et sasedev-games
|
# Plan 0.0.5 — réconciliation réelle avec KSP et sasedev-games
|
||||||
|
|
||||||
@@ -84,11 +84,13 @@ Cette tranche ne commence pas l'audit Rust ni le profil Tauri.
|
|||||||
|
|
||||||
Extraire le noyau réellement commun de `audit_rust_export_completeness.py`, l'aligner avec `510-RUST_CARGO.md`, documenter son caractère facultatif et ses limites, et ajouter des tests positifs/négatifs.
|
Extraire le noyau réellement commun de `audit_rust_export_completeness.py`, l'aligner avec `510-RUST_CARGO.md`, documenter son caractère facultatif et ses limites, et ajouter des tests positifs/négatifs.
|
||||||
|
|
||||||
La réconciliation a révélé que le script historique ne découvrait que `crates/*/Cargo.toml` : il couvrait les `22` crates KSP mais aucune des `23` crates Games imbriquées sous des sous-familles. La variante PGR doit donc corriger la découverte récursive avant toute généralisation. Elle doit également abandonner les sous-règles historiques de tests séparés et la duplication de résolution de noms déjà possédée par le compilateur.
|
La réconciliation a révélé que le script historique ne découvrait que `crates/*/Cargo.toml` : il couvrait les `22` packages KSP mais aucun des `23` packages Games imbriqués sous des sous-familles. La variante PGR corrige donc la découverte récursive et distingue aussi les cibles `lib`/`bin` d'un même package pour ne pas confondre plusieurs crates Rust compilées séparément.
|
||||||
|
|
||||||
Mesure de référence avec le scope PGR retenu : `KSP = 0 candidate(s) / 22 crates`, `Games = 2 candidate(s) / 23 crates`. Les deux signaux Games sont une observation de réconciliation, pas une non-conformité rétroactive.
|
La première livraison `alpha.5` a ensuite révélé un second défaut de scope pendant la revue opérateur : elle ne détectait que les chemins internes longs tels que `crate::module::Item`, alors que le contrat attendu impose `crate::Item` pour **toute référence intra-crate à un item partagé**, y compris un nom nu dans son module propriétaire. La gate mécanique était verte mais l'acceptation fonctionnelle est donc `FAIL`; `alpha.5.fix.1` corrige ce blind spot sans commencer Tauri.
|
||||||
|
|
||||||
Cette tranche ne réimporte pas `audit_rust_general_rules.py` en bloc. Toute règle déjà possédée par Rustfmt, Clippy, le compilateur ou les audits PGR existants reste chez son owner actuel.
|
Mesure `alpha.5.fix.1` en `--report-only --summary-only` : KSP `0.3.16` = `1525` lignes candidates `RUST-IMPORT-201` sur `22` packages ; Games `0.3.5` = `181` lignes candidates sur `23` packages (`2 RUST-API-201`, `179 RUST-IMPORT-201`). Ces nombres forment un inventaire d'adoption future, pas une non-conformité rétroactive des baselines.
|
||||||
|
|
||||||
|
Cette tranche ne réimporte pas `audit_rust_general_rules.py` en bloc et ne réintroduit pas le diagnostic historique de résolution `crate::X` déjà possédé par le compilateur. Toute autre règle déjà possédée par Rustfmt, Clippy, le compilateur ou les audits PGR existants reste chez son owner actuel.
|
||||||
|
|
||||||
### alpha.6 — premier profil framework : Tauri
|
### alpha.6 — premier profil framework : Tauri
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/510-RUST_CARGO.md -->
|
<!-- file: docs/rules/510-RUST_CARGO.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Profil Rust / Cargo
|
# Profil Rust / Cargo
|
||||||
|
|
||||||
@@ -90,9 +90,11 @@ Tout élément `pub` ou `pub(crate)` SHOULD posséder une rustdoc utile au point
|
|||||||
|
|
||||||
Une crate réutilisable SHOULD exposer son API durable depuis son crate-root par des réexports explicites. Les modules d'implémentation SHOULD rester privés ; `pub mod` ne doit pas être utilisé uniquement pour éviter de concevoir une façade stable.
|
Une crate réutilisable SHOULD exposer son API durable depuis son crate-root par des réexports explicites. Les modules d'implémentation SHOULD rester privés ; `pub mod` ne doit pas être utilisé uniquement pour éviter de concevoir une façade stable.
|
||||||
|
|
||||||
Un élément partagé à l'échelle de sa propre crate SHOULD être consommable via `crate::Item` après réexport approprié. Les autres crates consomment l'API publique du crate propriétaire plutôt que ses chemins internes.
|
Lorsqu'un projet adopte le **contrat strict de façade crate-root**, un élément `pub` d'un module fichier MUST être réexporté explicitement au crate-root. Un élément `pub(crate)` consommé hors de son module MUST de même posséder un réexport `pub(crate)` au crate-root. Un helper réellement local SHOULD rester privé plutôt qu'élargir artificiellement sa visibilité.
|
||||||
|
|
||||||
Les glob imports SHOULD être évités. Un alias de réexport ne doit pas servir à masquer durablement une collision de noms dans l'API ; lorsqu'un nom fait partie du contrat public, le propriétaire SHOULD choisir un nom canonique non ambigu.
|
Dès qu'un item possède cette identité crate-root, toute référence dans la même crate MUST utiliser `crate::Item`, y compris depuis son module de déclaration et depuis ses tests unitaires attachés. Les formes `Item`, `self::Item`, `super::Item`, `module::Item` et `crate::module::Item` ne sont pas des chemins de consommation canoniques pour cet item partagé. Un élément strictement privé reste local au module ; un test unitaire attaché peut alors utiliser `super::PrivateItem`.
|
||||||
|
|
||||||
|
Les autres crates consomment l'API publique du crate propriétaire via `owner_crate::Item` plutôt que ses chemins internes. Les glob imports SHOULD être évités. Un alias de réexport ne doit pas servir à masquer durablement une collision de noms dans l'API ; lorsqu'un nom fait partie du contrat public, le propriétaire SHOULD choisir un nom canonique non ambigu.
|
||||||
|
|
||||||
PGR ne généralise pas l'interdiction KSP plus stricte de tous les `use` non-traits : cette convention appartient à un profil ou projet plus spécifique tant qu'elle n'est pas nécessaire au contrat Rust général.
|
PGR ne généralise pas l'interdiction KSP plus stricte de tous les `use` non-traits : cette convention appartient à un profil ou projet plus spécifique tant qu'elle n'est pas nécessaire au contrat Rust général.
|
||||||
|
|
||||||
@@ -194,6 +196,6 @@ Un éventuel wrapper d'orchestration MAY appeler plusieurs audits, mais chaque a
|
|||||||
|
|
||||||
Les deux audits de style disponibles sont documentés dans [`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md). Ils restent optionnels et prennent un corpus Rust explicitement sélectionné.
|
Les deux audits de style disponibles sont documentés dans [`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md). Ils restent optionnels et prennent un corpus Rust explicitement sélectionné.
|
||||||
|
|
||||||
[`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md) documente séparément un audit facultatif de façade crate-root. Lorsqu'un projet choisit ce contrat strict, les éléments `pub` des modules fichiers et les éléments `pub(crate)` clairement partagés hors de leur module SHOULD être réexportés explicitement au crate-root ; lorsqu'un tel réexport existe, les chemins internes SHOULD employer `crate::Item` plutôt que `crate::module::Item`.
|
[`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md) documente séparément l'audit facultatif qui matérialise ce contrat strict. `RUST-API-201` vérifie la présence de la façade pour les items partagés ; `RUST-IMPORT-201` vérifie que les références intra-crate détectées passent réellement par `crate::Item`, y compris dans le module propriétaire, et pas seulement qu'un chemin interne long a été évité.
|
||||||
|
|
||||||
Cet audit de façade ne rend pas la règle obligatoire pour tous les projets Rust et ne remplace pas le compilateur. Le dépôt PGR lui-même ne contient pas de workspace Rust sur lequel une telle gate fournirait une preuve runtime ou Cargo réelle.
|
Cet audit ne rend pas le contrat strict obligatoire pour tous les projets Rust et ne remplace pas le compilateur. Il contrôle au contraire une convention architecturale que `rustc` et Clippy ne possèdent pas. Le dépôt PGR lui-même ne contient pas de workspace Rust sur lequel une telle gate fournirait une preuve runtime ou Cargo réelle.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/studies/001-KSP_GAMES_RECONCILIATION.md -->
|
<!-- file: docs/studies/001-KSP_GAMES_RECONCILIATION.md -->
|
||||||
<!-- version: 2 -->
|
<!-- version: 3 -->
|
||||||
|
|
||||||
# Étude de réconciliation PGR / KSP / sasedev-games
|
# Étude de réconciliation PGR / KSP / sasedev-games
|
||||||
|
|
||||||
@@ -137,7 +137,9 @@ Décision : classer ce contrôle `COMMON`, mais ne pas copier le script tel quel
|
|||||||
|
|
||||||
La relecture du script historique révèle une limite importante : sa découverte `crates/*/Cargo.toml` couvre les `22` crates KSP placées directement sous `crates/`, mais aucune des `23` crates Games imbriquées sous `crates/apps`, `crates/common`, `crates/engines`, etc. Le code des deux scripts est bien commun, mais le résultat historique Games `0 candidate(s)` n'était pas une preuve d'audit des crates réelles.
|
La relecture du script historique révèle une limite importante : sa découverte `crates/*/Cargo.toml` couvre les `22` crates KSP placées directement sous `crates/`, mais aucune des `23` crates Games imbriquées sous `crates/apps`, `crates/common`, `crates/engines`, etc. Le code des deux scripts est bien commun, mais le résultat historique Games `0 candidate(s)` n'était pas une preuve d'audit des crates réelles.
|
||||||
|
|
||||||
La variante PGR corrige la découverte récursive et réduit le scope aux deux propriétés effectivement possédées par `510-RUST_CARGO.md` : complétude de la façade crate-root et canonicalisation d'un chemin interne lorsqu'un réexport explicite existe. Avec cette variante, KSP donne `0 candidate(s)` sur `22` crates et Games `2 candidate(s)` sur `23` crates. Ces deux signaux Games ne sont pas des non-conformités rétroactives ; ils deviennent des points de décision lors d'une future adoption PGR.
|
La variante PGR corrige d'abord la découverte récursive. La revue opérateur de `alpha.5` montre ensuite que limiter la canonicalisation à `crate::module::Item` est insuffisant : les corpus contiennent de nombreuses références nues à des items pourtant réexportés, notamment dans leur module propriétaire, et ni `rustc` ni Clippy ne possèdent la convention « référence partagée = `crate::Item` partout dans la crate ». `alpha.5` est donc conservé `FAIL` et `alpha.5.fix.1` étend `RUST-IMPORT-201` à ce contrat réel.
|
||||||
|
|
||||||
|
La mesure corrigée, avec découverte récursive et séparation des cibles Cargo, donne `1525` lignes candidates `RUST-IMPORT-201` dans KSP sur `22` packages et `181` lignes candidates dans Games sur `23` packages (`2 RUST-API-201`, `179 RUST-IMPORT-201`). Ces volumes ne sont pas interprétés comme des défauts rétroactifs : ils montrent précisément pourquoi une adoption PGR d'un projet existant doit commencer en `--report-only`, puis migrer progressivement la convention avant d'en faire une gate bloquante.
|
||||||
|
|
||||||
Le grand `audit_rust_general_rules.py` des deux corpus reste `DEFER` comme source d'extraction, pas comme script à importer. Il mélange plusieurs responsabilités : headers, rustdoc, imports, structure de façade, blancs, constantes et règles locales ; Games y ajoute une exception FFI Android étroite. PGR doit continuer à extraire des audits monotâches seulement lorsqu'une propriété commune apporte une preuve non redondante.
|
Le grand `audit_rust_general_rules.py` des deux corpus reste `DEFER` comme source d'extraction, pas comme script à importer. Il mélange plusieurs responsabilités : headers, rustdoc, imports, structure de façade, blancs, constantes et règles locales ; Games y ajoute une exception FFI Android étroite. PGR doit continuer à extraire des audits monotâches seulement lorsqu'une propriété commune apporte une preuve non redondante.
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,111 @@
|
|||||||
<!-- file: docs/validation/003-RUST_EXPORT_COMPLETENESS.md -->
|
<!-- file: docs/validation/003-RUST_EXPORT_COMPLETENESS.md -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 2 -->
|
||||||
|
|
||||||
# Audit facultatif de façade Rust et export completeness
|
# Audit facultatif de façade Rust et canonicalisation intra-crate
|
||||||
|
|
||||||
## Responsabilité
|
## Responsabilité
|
||||||
|
|
||||||
`scripts/audit_rust_export_completeness.py` contrôle un contrat Rust optionnel plus strict que les garanties du compilateur : les éléments publics durables d'un module fichier doivent être exposés par une façade explicite au crate-root, et le code de la même crate doit employer cette façade lorsqu'un réexport correspondant existe.
|
`scripts/audit_rust_export_completeness.py` contrôle un contrat Rust optionnel plus strict que les garanties du compilateur et de Clippy : une crate qui adopte cette convention possède une façade crate-root explicite pour ses items partagés, et toute consommation intra-crate d'un item possédant cette identité canonique passe par `crate::Item`.
|
||||||
|
|
||||||
Le propriétaire normatif reste [`../rules/510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md). Le script ne définit pas une architecture à lui seul et ne doit être adopté que par un projet qui choisit ce contrat de façade.
|
Le propriétaire normatif reste [`../rules/510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md). Le script ne définit pas une architecture à lui seul ; il matérialise uniquement le contrat strict lorsqu'un projet choisit de l'adopter.
|
||||||
|
|
||||||
L'audit émet seulement deux familles de diagnostics :
|
L'audit émet deux familles de diagnostics :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RUST-API-201
|
RUST-API-201
|
||||||
élément `pub`, ou élément `pub(crate)` clairement consommé hors de son module fichier,
|
item `pub` d'un module fichier sans réexport explicite au crate-root,
|
||||||
sans réexport explicite correspondant au crate-root
|
ou item `pub(crate)` clairement consommé hors de son module sans réexport crate-root
|
||||||
|
|
||||||
RUST-IMPORT-201
|
RUST-IMPORT-201
|
||||||
chemin interne long `crate::module::Item` alors que le crate-root expose déjà `crate::Item`
|
référence intra-crate non canonique à un item possédant une identité crate-root ;
|
||||||
|
la forme attendue est `crate::Item`
|
||||||
```
|
```
|
||||||
|
|
||||||
Il ne vérifie pas les règles générales de style, les lignes vides, l'ordre des constantes, la rustdoc, les lints Cargo/Clippy, la sûreté FFI ou le contenu métier.
|
`RUST-IMPORT-201` ne se limite donc pas à `crate::module::Item`. Il couvre notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Item
|
||||||
|
self::Item
|
||||||
|
super::Item
|
||||||
|
module::Item
|
||||||
|
crate::module::Item
|
||||||
|
```
|
||||||
|
|
||||||
|
lorsque ces formes désignent un item partagé déjà exposé comme `crate::Item`.
|
||||||
|
|
||||||
|
La règle s'applique également **dans le module qui déclare l'item**. Par exemple :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Runtime;
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
pub fn new() -> Runtime {
|
||||||
|
return Runtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
n'est pas canonique après réexport de `Runtime`. Le contrat strict attend :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Runtime;
|
||||||
|
|
||||||
|
impl crate::Runtime {
|
||||||
|
pub fn new() -> crate::Runtime {
|
||||||
|
return crate::Runtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le même principe vaut pour une fonction partagée appelée depuis son module propriétaire : `crate::build()` est canonique, `build()` ne l'est pas.
|
||||||
|
|
||||||
|
Les lignes de déclaration de l'item et les lignes de réexport qui construisent la façade sont naturellement exemptées. Un item strictement privé n'appartient pas à ce contrat ; dans un test unitaire attaché à son module parent, `super::PrivateItem` reste une forme locale légitime.
|
||||||
|
|
||||||
|
## Pourquoi Cargo/Clippy ne suffisent pas
|
||||||
|
|
||||||
|
Le compilateur vérifie la résolution et la visibilité des noms ; il ne possède pas la convention « tout accès partagé passe par `crate::Item` ». Clippy peut signaler certains imports ou réexports inutilisés, mais une seule consommation correcte de `crate::Item` peut suffire à rendre le réexport utilisé tandis que d'autres références continuent d'employer un nom nu ou un chemin interne.
|
||||||
|
|
||||||
|
L'audit fournit donc une preuve distincte : **uniformité de la façade et des chemins intra-crate**, pas simple compilabilité.
|
||||||
|
|
||||||
|
## Cibles Cargo et graphes de modules
|
||||||
|
|
||||||
|
La `version: 2` ne confond plus automatiquement tous les fichiers d'un package Cargo dans une seule crate. Un package peut contenir simultanément une bibliothèque `src/lib.rs` et un binaire `src/main.rs` ; ces deux cibles sont compilées comme des crates distinctes et `crate::Item` n'y désigne pas la même racine.
|
||||||
|
|
||||||
|
L'audit découvre les packages récursivement puis traite séparément les cibles standard/explicites `lib` et `bin`. Pour chaque cible, il suit les modules fichiers déclarés par `mod name;` et les modules `#[path = "..."]`, ce qui couvre notamment les `unit_tests/` attachés utilisés dans le corpus KSP/Games sans englober les tests d'intégration Cargo sous `tests/`, qui sont des crates externes.
|
||||||
|
|
||||||
|
Cette distinction évite de considérer à tort `owner_crate::Item` utilisé dans un binaire comme une violation de la bibliothèque du même package.
|
||||||
|
|
||||||
## Provenance et correction du corpus
|
## Provenance et correction du corpus
|
||||||
|
|
||||||
KSP `0.3.16` et Games `0.3.5` contiennent un `audit_rust_export_completeness.py` dont le corps est identique à l'exception de sa révision de fichier. Cette convergence justifie l'extraction du concept, mais pas une copie littérale.
|
KSP `0.3.16` et Games `0.3.5` contiennent un `audit_rust_export_completeness.py` historiquement identique à l'exception de sa révision. Cette convergence justifie l'extraction du concept, mais pas une copie littérale.
|
||||||
|
|
||||||
Le script historique découvre uniquement `crates/*/Cargo.toml`. Cette hypothèse couvre les `22` crates KSP situées directement sous `crates/`, mais elle ne couvre aucune des `23` crates Games organisées sous des sous-familles telles que `crates/apps/`, `crates/common/` ou `crates/engines/`. Un résultat historique `0 candidate(s)` dans Games ne constitue donc pas une preuve que ses crates avaient réellement été inspectées par cet audit.
|
Deux limites historiques ont été identifiées :
|
||||||
|
|
||||||
La variante PGR corrige ce point en découvrant récursivement les packages Cargo possédant `src/lib.rs` ou `src/main.rs`, tout en ignorant les arbres générés usuels. Elle gère également correctement un module fichier `mod.rs` et les réexports explicites simples, groupés ou aliasés.
|
1. la découverte `crates/*/Cargo.toml` couvrait les `22` packages KSP directement sous `crates/`, mais aucune des `23` crates Games imbriquées sous `crates/apps/`, `crates/common/`, `crates/engines/`, etc. ;
|
||||||
|
2. la canonicalisation détectait surtout `crate::module::Item` et quelques cas particuliers de tests, mais pas les références ordinaires par nom nu dans les modules de production, y compris le module propriétaire.
|
||||||
|
|
||||||
La réconciliation générateur réalisée pendant `0.0.5-alpha.5` donne avec la nouvelle découverte :
|
La première version PGR de `alpha.5` corrigeait seulement la découverte récursive et le chemin interne long. La revue opérateur après sa gate a explicitement révélé la seconde lacune ; `alpha.5` est donc conservé `FAIL` et `alpha.5.fix.1` étend `RUST-IMPORT-201` au contrat réellement attendu.
|
||||||
|
|
||||||
|
## Mesure de réconciliation après le fix
|
||||||
|
|
||||||
|
Avec la `version: 2`, en mode `--report-only --summary-only`, les baselines autoritaires de la réconciliation donnent :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
KSP 0.3.16 22 crate(s) 0 candidate(s)
|
KSP 0.3.16
|
||||||
Games 0.3.5 23 crate(s) 2 candidate(s)
|
22 package(s)
|
||||||
|
1525 candidate(s)
|
||||||
|
RUST-IMPORT-201: 1525
|
||||||
|
|
||||||
|
Games 0.3.5
|
||||||
|
23 package(s)
|
||||||
|
181 candidate(s)
|
||||||
|
RUST-API-201: 2
|
||||||
|
RUST-IMPORT-201: 179
|
||||||
```
|
```
|
||||||
|
|
||||||
Les deux candidats Games sont des constantes `pub(crate)` du smoke WebTransport navigateur consommées depuis d'autres modules sans façade crate-root. Games n'étant pas encore gouverné par PGR, ces signaux ne sont pas qualifiés de défauts rétroactifs ; ils montrent simplement ce qu'une adoption future de ce contrat devrait décider ou corriger.
|
Ces nombres représentent des **lignes candidates**, pas des déclarations distinctes. Ils sont beaucoup plus élevés que la première mesure `0 / 2` parce que le contrat corrige précisément le blind spot signalé : les références nues déjà répandues dans les modules propriétaires et consumers deviennent enfin visibles.
|
||||||
|
|
||||||
## Ce qui n'est pas repris du script historique
|
KSP et Games n'étant pas encore gouvernés par cette version de PGR, ces résultats ne sont pas qualifiés de défauts rétroactifs et ne déclenchent aucune migration dans `0.0.5`. Ils constituent un inventaire d'adoption future. Pour un projet existant, l'usage initial en `--report-only` est donc particulièrement important.
|
||||||
|
|
||||||
PGR n'importe pas les diagnostics historiques liés aux tests unitaires séparés (`super::...`, références non qualifiées) : leur validité dépend du layout et du contrat local des modules de test, et la baseline Games ne prouve pas une convergence opérationnelle sur ce point.
|
|
||||||
|
|
||||||
PGR n'importe pas non plus le contrôle d'un `crate::Symbol` non résolu. Le compilateur Rust est déjà propriétaire de la résolution de noms ; dupliquer cette erreur dans un audit Python n'apporte pas une preuve supplémentaire proportionnée.
|
|
||||||
|
|
||||||
L'audit PGR reste donc monotâche : façade crate-root et canonicalisation des chemins pour les seuls symboles qu'il sait relier explicitement.
|
|
||||||
|
|
||||||
## Adoption
|
## Adoption
|
||||||
|
|
||||||
@@ -64,20 +124,22 @@ Commande canonique :
|
|||||||
python3 -B scripts/audit_rust_export_completeness.py --root .
|
python3 -B scripts/audit_rust_export_completeness.py --root .
|
||||||
```
|
```
|
||||||
|
|
||||||
Le code de sortie est `0` lorsque le corpus est propre, `1` lorsqu'au moins un candidat est trouvé et `2` lorsque la racine est invalide, illisible ou ne contient aucune crate Rust détectable. `--report-only` permet une phase d'inventaire sans bloquer la gate ; `--summary-only` réduit la sortie aux comptes.
|
Le code de sortie est `0` lorsque le corpus est propre, `1` lorsqu'au moins un candidat est trouvé et `2` lorsque la racine est invalide, illisible ou ne contient aucun package Rust détectable. `--report-only` permet un inventaire sans bloquer la gate ; `--summary-only` réduit la sortie aux comptes.
|
||||||
|
|
||||||
Une adoption progressive peut donc commencer par :
|
Une adoption progressive d'un projet existant devrait commencer par :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -B scripts/audit_rust_export_completeness.py --root . --report-only
|
python3 -B scripts/audit_rust_export_completeness.py --root . --report-only --summary-only
|
||||||
```
|
```
|
||||||
|
|
||||||
puis devenir bloquante une fois les exceptions ou écarts locaux explicitement résolus.
|
puis inventorier/corriger les familles d'écarts avant de rendre l'audit bloquant. Une nouvelle crate créée sous le contrat peut au contraire l'activer immédiatement.
|
||||||
|
|
||||||
## Limites
|
## Limites
|
||||||
|
|
||||||
L'analyse est lexicale et non un parseur Rust complet. Elle vise les déclarations `pub`/`pub(crate)` au niveau racine de modules fichiers et les réexports explicites du crate-root. Les macros générant des items, attributs élaborés, modules inline complexes, `#[path]` non standard et formes de réexport imbriquées au-delà du contrat testé peuvent nécessiter une revue humaine ou les outils Rust du projet.
|
L'analyse reste lexicale et ne remplace pas un parseur Rust complet. Elle vise les déclarations module-level `pub`/`pub(crate)`, les réexports explicites et les références dans le graphe de modules d'une cible. Les macros générant des items, modules inline complexes, `include!`, formes de `#[path]` atypiques et réexports élaborés peuvent nécessiter une revue humaine.
|
||||||
|
|
||||||
Un élément `pub(crate)` n'est exigé à la façade que lorsqu'une consommation externe au fichier est clairement détectée par un chemin `crate::module::Item` ou `crate::Item`. L'absence de diagnostic ne prouve donc pas toute la sémantique de visibilité de la crate.
|
Pour réduire les faux positifs, une fonction partagée est reconnue comme référence lorsqu'elle apparaît sous une forme d'appel directe ; l'usage d'une fonction comme valeur de premier ordre peut donc échapper à cette version. Les chemins explicitement qualifiés par une crate externe et les appels de méthodes `.name()` ne sont pas assimilés à un item local de même nom.
|
||||||
|
|
||||||
L'audit ne remplace ni `cargo check`, ni Clippy, ni rustdoc. Il complète uniquement une convention architecturale explicitement choisie.
|
Un item `pub(crate)` qui n'est pas clairement consommé hors de son module n'est pas forcé au crate-root par `RUST-API-201`; sa visibilité peut nécessiter une revue séparée. Dès qu'un réexport crate-root existe, ses références détectées sont toutefois soumises à `RUST-IMPORT-201`, y compris dans son module de déclaration.
|
||||||
|
|
||||||
|
L'audit ne remplace ni `cargo check`, ni Clippy, ni rustdoc. Il complète ces outils avec une convention architecturale qu'ils ne possèdent pas.
|
||||||
|
|||||||
110
history/0.0.5/alpha.5.fix.1.md
Normal file
110
history/0.0.5/alpha.5.fix.1.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<!-- file: history/0.0.5/alpha.5.fix.1.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# History 0.0.5-alpha.5.fix.1 — canonicalisation réelle des références crate-root
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
PASS
|
||||||
|
|
||||||
|
La gate opérateur de `0.0.5-alpha.5.fix.1` exécutée le `2026-09-25` valide la correction du contrat Rust de façade : un item partagé exposé au crate-root doit être référencé via `crate::Item` dans toute la crate, y compris depuis son module de déclaration. Les tests ciblés/globaux, audits Markdown, contrôle structurel et contrôles Git sont propres ; le scope indexé correspond exactement au fix.
|
||||||
|
|
||||||
|
## Baseline et version
|
||||||
|
|
||||||
|
La tranche part du commit opérateur de `0.0.5-alpha.5` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
f03f864 0.0.5-alpha.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Le push vers `origin/master` a réussi avant l'application de `alpha.5.fix.1`.
|
||||||
|
|
||||||
|
La version contrôlée après application du fix est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.5-alpha.5.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
`history/0.0.5/alpha.5.md` est présent avec statut `FAIL`, `deltas/0.0.5/alpha.5.fix.1.md` est présent et `history/0.0.5/alpha.5.fix.1.md` était absent pendant la gate, conformément au workflow.
|
||||||
|
|
||||||
|
## Validation opérateur observée
|
||||||
|
|
||||||
|
L'opérateur a fourni les sorties du `2026-09-25` depuis `~/Projects/pgr`, à `00:36:32`–`00:37:30`.
|
||||||
|
|
||||||
|
Les contrôles de révision ont confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
scripts/audit_rust_export_completeness.py version 2
|
||||||
|
tests/test_rust_export_completeness.py version 2
|
||||||
|
docs/validation/003-RUST_EXPORT_COMPLETENESS.md version 2
|
||||||
|
docs/rules/510-RUST_CARGO.md version 4
|
||||||
|
```
|
||||||
|
|
||||||
|
La suite ciblée a exécuté `18` tests, tous réussis :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ran 18 tests in 0.161s
|
||||||
|
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
|
||||||
|
La suite Python globale a exécuté `37` tests, tous réussis :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ran 37 tests in 0.514s
|
||||||
|
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Les trois audits documentaires affichent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Markdown header audit: clean (142 file(s))
|
||||||
|
Markdown table audit: clean (6 table(s), 142 file(s))
|
||||||
|
Markdown link audit: clean (137 local link(s), 142 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
Le contrôle structurel spécifique affiche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.5 alpha.5.fix.1 canonical Rust path audit: clean (owner module, cross-module, attached tests, distinct Cargo targets)
|
||||||
|
```
|
||||||
|
|
||||||
|
Il confirme notamment que l'audit possède les cibles Cargo distinctes, suit les modules associés et couvre `RUST-API-201` ainsi que `RUST-IMPORT-201` pour les références intra-crate non canoniques. La documentation normative impose `crate::Item` y compris depuis le module propriétaire.
|
||||||
|
|
||||||
|
`git diff --check` et `git diff --cached --check` n'ont produit aucun diagnostic.
|
||||||
|
|
||||||
|
Le scope Git observé avant ajout du présent history est exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
M VERSION
|
||||||
|
A deltas/0.0.5/alpha.5.fix.1.md
|
||||||
|
M docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md
|
||||||
|
M docs/rules/510-RUST_CARGO.md
|
||||||
|
M docs/studies/001-KSP_GAMES_RECONCILIATION.md
|
||||||
|
M docs/validation/003-RUST_EXPORT_COMPLETENESS.md
|
||||||
|
M scripts/audit_rust_export_completeness.py
|
||||||
|
M tests/test_rust_export_completeness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
La transcription du terminal intercale quelques fragments du heredoc de contrôle avec la sortie de `unittest`. Les dix-huit tests ciblés, les trente-sept tests globaux, les trois audits Markdown, le contrôle structurel final et le scope Git restent identifiables et ne signalent aucun échec.
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
|
||||||
|
Le blind spot fonctionnel de `alpha.5` est fermé. L'audit PGR distingue les targets Cargo et impose la façade crate-root sur les références intra-crate détectables sans prétendre remplacer un parseur Rust.
|
||||||
|
|
||||||
|
Les mesures KSP/Games consignées par le fix restent des inventaires d'adoption future en `--report-only`, pas des non-conformités rétroactives.
|
||||||
|
|
||||||
|
Aucun nouveau fix n'est requis.
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Appliquer le présent history puis committer et pousser `0.0.5-alpha.5.fix.1`.
|
||||||
|
|
||||||
|
La tranche suivante est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.0.5-alpha.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle doit rester limitée à l'activation de la famille `700 — frameworks` et au premier profil Tauri réellement commun aux baselines KSP/Games. Elle ne doit pas créer prématurément un skeleton Tauri ni imposer Vite, WASM, npm, des ports, fenêtres, plugins ou layouts applicatifs particuliers.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# file: scripts/audit_rust_export_completeness.py
|
# file: scripts/audit_rust_export_completeness.py
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
"""Audit crate-root façade completeness and canonical same-crate paths."""
|
"""Audit Rust crate-root façade completeness and canonical same-crate references."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ import dataclasses
|
|||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import tomllib
|
||||||
|
|
||||||
from _rust_audit_common import line_depths, mask_source
|
from _rust_audit_common import line_depths, mask_source
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ _IGNORED_DIRECTORY_NAMES = frozenset({
|
|||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Candidate:
|
class Candidate:
|
||||||
"""One façade or same-crate path violation candidate."""
|
"""One façade or canonical-reference violation candidate."""
|
||||||
|
|
||||||
code: str
|
code: str
|
||||||
path: str
|
path: str
|
||||||
@@ -46,6 +47,24 @@ class Declaration:
|
|||||||
kind: str
|
kind: str
|
||||||
path: pathlib.Path
|
path: pathlib.Path
|
||||||
line: int
|
line: int
|
||||||
|
column: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Target:
|
||||||
|
"""One Rust crate target and the module files reachable from its root."""
|
||||||
|
|
||||||
|
package: pathlib.Path
|
||||||
|
root: pathlib.Path
|
||||||
|
modules: tuple[tuple[pathlib.Path, str], ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class CanonicalItem:
|
||||||
|
"""One visible item with its canonical crate-root name."""
|
||||||
|
|
||||||
|
declaration: Declaration
|
||||||
|
canonical_name: str
|
||||||
|
|
||||||
|
|
||||||
def _is_ignored(path: pathlib.Path, root: pathlib.Path) -> bool:
|
def _is_ignored(path: pathlib.Path, root: pathlib.Path) -> bool:
|
||||||
@@ -66,35 +85,147 @@ def crate_roots(root: pathlib.Path) -> list[pathlib.Path]:
|
|||||||
if _is_ignored(manifest, root):
|
if _is_ignored(manifest, root):
|
||||||
continue
|
continue
|
||||||
crate = manifest.parent
|
crate = manifest.parent
|
||||||
if (crate / "src/lib.rs").is_file() or (crate / "src/main.rs").is_file():
|
if (crate / "src/lib.rs").is_file() or (crate / "src/main.rs").is_file() or _explicit_target_paths(crate):
|
||||||
crates.add(crate)
|
crates.add(crate)
|
||||||
return sorted(crates)
|
return sorted(crates)
|
||||||
|
|
||||||
|
|
||||||
def crate_root_source(crate: pathlib.Path) -> pathlib.Path | None:
|
def _explicit_target_paths(crate: pathlib.Path) -> tuple[pathlib.Path, ...]:
|
||||||
"""Return the library or binary crate-root source when present."""
|
"""Return existing explicit lib/bin target paths declared in Cargo.toml."""
|
||||||
|
|
||||||
|
manifest = crate / "Cargo.toml"
|
||||||
|
if not manifest.is_file():
|
||||||
|
return ()
|
||||||
|
try:
|
||||||
|
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, tomllib.TOMLDecodeError):
|
||||||
|
return ()
|
||||||
|
paths: set[pathlib.Path] = set()
|
||||||
|
lib = data.get("lib")
|
||||||
|
if isinstance(lib, dict) and isinstance(lib.get("path"), str):
|
||||||
|
candidate = crate / lib["path"]
|
||||||
|
if candidate.is_file():
|
||||||
|
paths.add(candidate)
|
||||||
|
bins = data.get("bin", [])
|
||||||
|
if isinstance(bins, list):
|
||||||
|
for entry in bins:
|
||||||
|
if isinstance(entry, dict) and isinstance(entry.get("path"), str):
|
||||||
|
candidate = crate / entry["path"]
|
||||||
|
if candidate.is_file():
|
||||||
|
paths.add(candidate)
|
||||||
|
return tuple(sorted(paths))
|
||||||
|
|
||||||
|
|
||||||
|
def target_roots(crate: pathlib.Path) -> list[pathlib.Path]:
|
||||||
|
"""Return standard and explicitly configured Rust crate target roots."""
|
||||||
|
|
||||||
|
roots: set[pathlib.Path] = set(_explicit_target_paths(crate))
|
||||||
|
manifest = crate / "Cargo.toml"
|
||||||
|
package: dict[str, object] = {}
|
||||||
|
if manifest.is_file():
|
||||||
|
try:
|
||||||
|
parsed = tomllib.loads(manifest.read_text(encoding="utf-8"))
|
||||||
|
raw_package = parsed.get("package", {})
|
||||||
|
if isinstance(raw_package, dict):
|
||||||
|
package = raw_package
|
||||||
|
except (OSError, tomllib.TOMLDecodeError):
|
||||||
|
package = {}
|
||||||
|
|
||||||
library = crate / "src/lib.rs"
|
library = crate / "src/lib.rs"
|
||||||
if library.is_file():
|
if library.is_file():
|
||||||
return library
|
roots.add(library)
|
||||||
binary = crate / "src/main.rs"
|
|
||||||
if binary.is_file():
|
if package.get("autobins", True) is not False:
|
||||||
return binary
|
main = crate / "src/main.rs"
|
||||||
|
if main.is_file():
|
||||||
|
roots.add(main)
|
||||||
|
bin_dir = crate / "src/bin"
|
||||||
|
if bin_dir.is_dir():
|
||||||
|
roots.update(path for path in bin_dir.glob("*.rs") if path.is_file())
|
||||||
|
roots.update(path for path in bin_dir.glob("*/main.rs") if path.is_file())
|
||||||
|
return sorted(roots)
|
||||||
|
|
||||||
|
|
||||||
|
def _standard_child_module_path(current: pathlib.Path, target_root: pathlib.Path, name: str) -> pathlib.Path | None:
|
||||||
|
"""Resolve a standard `mod name;` file relative to one source module."""
|
||||||
|
|
||||||
|
if current == target_root or current.name == "mod.rs":
|
||||||
|
base = current.parent
|
||||||
|
else:
|
||||||
|
base = current.parent / current.stem
|
||||||
|
direct = base / f"{name}.rs"
|
||||||
|
nested = base / name / "mod.rs"
|
||||||
|
if direct.is_file():
|
||||||
|
return direct
|
||||||
|
if nested.is_file():
|
||||||
|
return nested
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def module_path(crate: pathlib.Path, path: pathlib.Path) -> str:
|
def _module_children(path: pathlib.Path, target_root: pathlib.Path) -> list[tuple[pathlib.Path, str]]:
|
||||||
"""Return the file-based module path represented by one Rust source file."""
|
"""Return top-level external module declarations from one Rust source file."""
|
||||||
|
|
||||||
relative = path.relative_to(crate / "src").with_suffix("")
|
text = path.read_text(encoding="utf-8")
|
||||||
parts = list(relative.parts)
|
lines = text.splitlines()
|
||||||
if parts and parts[-1] == "mod":
|
masked, _ = mask_source(text)
|
||||||
parts.pop()
|
masked_lines = masked.splitlines()
|
||||||
return "::".join(parts)
|
depths = line_depths(masked)
|
||||||
|
module_pattern = re.compile(r"^\s*(?:(?:pub|pub\([^)]*\))\s+)?mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;")
|
||||||
|
path_pattern = re.compile(r'^\s*#\[path\s*=\s*"([^"]+)"\]\s*$')
|
||||||
|
found: list[tuple[pathlib.Path, str]] = []
|
||||||
|
for index, masked_line in enumerate(masked_lines):
|
||||||
|
if index >= len(depths) or depths[index] != 0:
|
||||||
|
continue
|
||||||
|
match = module_pattern.match(masked_line)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
module_name = match.group(1)
|
||||||
|
explicit: pathlib.Path | None = None
|
||||||
|
probe = index - 1
|
||||||
|
while probe >= 0 and lines[probe].lstrip().startswith("#["):
|
||||||
|
path_match = path_pattern.match(lines[probe])
|
||||||
|
if path_match is not None:
|
||||||
|
candidate = (path.parent / path_match.group(1)).resolve()
|
||||||
|
if candidate.is_file():
|
||||||
|
explicit = candidate
|
||||||
|
break
|
||||||
|
probe -= 1
|
||||||
|
child = explicit or _standard_child_module_path(path, target_root, module_name)
|
||||||
|
if child is not None:
|
||||||
|
found.append((child.resolve(), module_name))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]:
|
def target_modules(crate: pathlib.Path, target_root: pathlib.Path) -> dict[pathlib.Path, str]:
|
||||||
"""Return root-level public or crate-public declarations from one module file."""
|
"""Return module files reachable from one target root and their logical paths."""
|
||||||
|
|
||||||
|
root = target_root.resolve()
|
||||||
|
modules: dict[pathlib.Path, str] = {root: ""}
|
||||||
|
pending: list[pathlib.Path] = [root]
|
||||||
|
while pending:
|
||||||
|
current = pending.pop()
|
||||||
|
parent_module = modules[current]
|
||||||
|
for child, name in _module_children(current, root):
|
||||||
|
if _is_ignored(child, crate) or child in modules:
|
||||||
|
continue
|
||||||
|
module = f"{parent_module}::{name}" if parent_module else name
|
||||||
|
modules[child] = module
|
||||||
|
pending.append(child)
|
||||||
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def targets(crate: pathlib.Path) -> list[Target]:
|
||||||
|
"""Return independently compiled crate targets for one Cargo package."""
|
||||||
|
|
||||||
|
found: list[Target] = []
|
||||||
|
for root in target_roots(crate):
|
||||||
|
modules = target_modules(crate, root)
|
||||||
|
found.append(Target(crate, root.resolve(), tuple(sorted(modules.items(), key=lambda item: item[0].as_posix()))))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def declaration_candidates(path: pathlib.Path, module: str) -> list[Declaration]:
|
||||||
|
"""Return top-level public or crate-public declarations from one module file."""
|
||||||
|
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
@@ -115,12 +246,13 @@ def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Decl
|
|||||||
continue
|
continue
|
||||||
found.append(
|
found.append(
|
||||||
Declaration(
|
Declaration(
|
||||||
module=module_path(crate, path),
|
module=module,
|
||||||
name=match.group(3),
|
name=match.group(3),
|
||||||
visibility=match.group(1),
|
visibility=match.group(1),
|
||||||
kind=match.group(2),
|
kind=match.group(2),
|
||||||
path=path,
|
path=path,
|
||||||
line=line_number,
|
line=line_number,
|
||||||
|
column=match.start(3),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return found
|
return found
|
||||||
@@ -143,15 +275,11 @@ def _parse_export_item(module: str, item: str) -> tuple[tuple[str, str], str] |
|
|||||||
return (module, name), alias
|
return (module, name), alias
|
||||||
|
|
||||||
|
|
||||||
def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
|
def root_exports(root_source: pathlib.Path) -> dict[tuple[str, str], str]:
|
||||||
"""Return explicit crate-root re-exports keyed by source module and symbol."""
|
"""Return explicit crate-root re-exports keyed by source module and symbol."""
|
||||||
|
|
||||||
crate_root = crate_root_source(crate)
|
|
||||||
exports: dict[tuple[str, str], str] = {}
|
exports: dict[tuple[str, str], str] = {}
|
||||||
if crate_root is None:
|
text = root_source.read_text(encoding="utf-8")
|
||||||
return exports
|
|
||||||
|
|
||||||
text = crate_root.read_text(encoding="utf-8")
|
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r"^\s*pub(?:\(crate\))?\s+use\s+(?:self|crate)::(.*?);",
|
r"^\s*pub(?:\(crate\))?\s+use\s+(?:self|crate)::(.*?);",
|
||||||
re.MULTILINE | re.DOTALL,
|
re.MULTILINE | re.DOTALL,
|
||||||
@@ -177,75 +305,171 @@ def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
|
|||||||
return exports
|
return exports
|
||||||
|
|
||||||
|
|
||||||
def externally_used(crate: pathlib.Path, declaration: Declaration) -> bool:
|
def _reexport_lines(root_source: pathlib.Path) -> set[int]:
|
||||||
"""Return whether a crate-public item is clearly used outside its module file."""
|
"""Return crate-root line numbers occupied by explicit re-export statements."""
|
||||||
|
|
||||||
|
text = root_source.read_text(encoding="utf-8")
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^\s*pub(?:\(crate\))?\s+use\s+(?:self|crate)::.*?;",
|
||||||
|
re.MULTILINE | re.DOTALL,
|
||||||
|
)
|
||||||
|
lines: set[int] = set()
|
||||||
|
for match in pattern.finditer(text):
|
||||||
|
start = text.count("\n", 0, match.start()) + 1
|
||||||
|
end = text.count("\n", 0, match.end()) + 1
|
||||||
|
lines.update(range(start, end + 1))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _target_declarations(target: Target) -> list[Declaration]:
|
||||||
|
"""Return visible module-level declarations for one crate target."""
|
||||||
|
|
||||||
|
return [
|
||||||
|
declaration
|
||||||
|
for path, module in target.modules
|
||||||
|
for declaration in declaration_candidates(path, module)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _external_usage(target: Target, declaration: Declaration) -> bool:
|
||||||
|
"""Return whether a crate-public item is clearly referenced from another module file."""
|
||||||
|
|
||||||
|
if not declaration.module:
|
||||||
|
return True
|
||||||
direct = f"crate::{declaration.module}::{declaration.name}"
|
direct = f"crate::{declaration.module}::{declaration.name}"
|
||||||
root_direct = f"crate::{declaration.name}"
|
root_direct = f"crate::{declaration.name}"
|
||||||
for path in sorted(crate.rglob("*.rs")):
|
super_direct = f"super::{declaration.name}"
|
||||||
if _is_ignored(path, crate) or path.resolve() == declaration.path.resolve():
|
for path, _ in target.modules:
|
||||||
|
if path.resolve() == declaration.path.resolve():
|
||||||
continue
|
continue
|
||||||
masked, _ = mask_source(path.read_text(encoding="utf-8"))
|
masked, _ = mask_source(path.read_text(encoding="utf-8"))
|
||||||
if direct in masked or root_direct in masked:
|
if direct in masked or root_direct in masked or super_direct in masked:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
|
def _declaration_lookup(declarations: list[Declaration]) -> dict[tuple[pathlib.Path, int, str], Declaration]:
|
||||||
"""Audit one Rust package for the selected strict façade contract."""
|
"""Index declaration-name occurrences that must not be treated as references."""
|
||||||
|
|
||||||
crate_root = crate_root_source(crate)
|
return {(item.path.resolve(), item.line, item.name): item for item in declarations}
|
||||||
if crate_root is None:
|
|
||||||
|
|
||||||
|
def _canonical_reference_candidates(
|
||||||
|
workspace: pathlib.Path,
|
||||||
|
target: Target,
|
||||||
|
declarations: list[Declaration],
|
||||||
|
exports: dict[tuple[str, str], str],
|
||||||
|
) -> list[Candidate]:
|
||||||
|
"""Return non-canonical references to items that have a crate-root identity."""
|
||||||
|
|
||||||
|
items: list[CanonicalItem] = []
|
||||||
|
for declaration in declarations:
|
||||||
|
if declaration.path.resolve() == target.root and not declaration.module:
|
||||||
|
items.append(CanonicalItem(declaration, declaration.name))
|
||||||
|
continue
|
||||||
|
alias = exports.get((declaration.module, declaration.name))
|
||||||
|
if alias is not None:
|
||||||
|
items.append(CanonicalItem(declaration, alias))
|
||||||
|
|
||||||
|
by_token: dict[str, list[CanonicalItem]] = {}
|
||||||
|
for item in items:
|
||||||
|
by_token.setdefault(item.declaration.name, []).append(item)
|
||||||
|
if item.canonical_name != item.declaration.name:
|
||||||
|
by_token.setdefault(item.canonical_name, []).append(item)
|
||||||
|
if not by_token:
|
||||||
return []
|
return []
|
||||||
exports = root_exports(crate)
|
|
||||||
|
token_pattern = re.compile(r"\b(?:" + "|".join(re.escape(name) for name in sorted(by_token, key=len, reverse=True)) + r")\b")
|
||||||
|
declaration_lookup = _declaration_lookup(declarations)
|
||||||
|
root_reexport_lines = _reexport_lines(target.root)
|
||||||
|
internal_roots = {segment for _, module in target.modules if module for segment in module.split("::")}
|
||||||
candidates: list[Candidate] = []
|
candidates: list[Candidate] = []
|
||||||
|
|
||||||
for path in sorted((crate / "src").rglob("*.rs")):
|
for path, _ in target.modules:
|
||||||
if path == crate_root or _is_ignored(path, crate):
|
|
||||||
continue
|
|
||||||
for declaration in declaration_candidates(crate, path):
|
|
||||||
required = declaration.visibility == "pub" or externally_used(crate, declaration)
|
|
||||||
if required and (declaration.module, declaration.name) not in exports:
|
|
||||||
reason = "public item" if declaration.visibility == "pub" else "crate-public item used outside its module"
|
|
||||||
candidates.append(
|
|
||||||
Candidate(
|
|
||||||
code="RUST-API-201",
|
|
||||||
path=path.relative_to(workspace).as_posix(),
|
|
||||||
line=declaration.line,
|
|
||||||
message=f"{reason} `{declaration.name}` requires an explicit crate-root re-export",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
long_path = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)")
|
|
||||||
for path in sorted(crate.rglob("*.rs")):
|
|
||||||
if path == crate_root or _is_ignored(path, crate):
|
|
||||||
continue
|
|
||||||
relative = path.relative_to(workspace).as_posix()
|
relative = path.relative_to(workspace).as_posix()
|
||||||
masked, _ = mask_source(path.read_text(encoding="utf-8"))
|
masked, _ = mask_source(path.read_text(encoding="utf-8"))
|
||||||
for line_number, line in enumerate(masked.splitlines(), 1):
|
for line_number, line in enumerate(masked.splitlines(), 1):
|
||||||
for match in long_path.finditer(line):
|
for match in token_pattern.finditer(line):
|
||||||
alias = exports.get((match.group(1), match.group(2)))
|
token = match.group(0)
|
||||||
if alias is None:
|
for item in by_token[token]:
|
||||||
|
declaration = item.declaration
|
||||||
|
if declaration.kind == "fn" and re.match(r"\s*(?:\(|::<)", line[match.end():]) is None:
|
||||||
continue
|
continue
|
||||||
|
prefix = line[:match.start()]
|
||||||
|
if re.search(r"(?:const|static|type|struct|enum|trait|union|fn)\s+$", prefix) is not None:
|
||||||
|
continue
|
||||||
|
if (path.resolve(), line_number, token) in declaration_lookup and token == declaration.name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if path.resolve() == target.root and line_number in root_reexport_lines:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if token == item.canonical_name and re.search(r"\bcrate::\s*$", prefix) is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if match.start() > 0 and line[match.start() - 1] == ".":
|
||||||
|
continue
|
||||||
|
|
||||||
|
qualified = re.search(
|
||||||
|
r"([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)::\s*$",
|
||||||
|
prefix,
|
||||||
|
)
|
||||||
|
if qualified is not None:
|
||||||
|
first = qualified.group(1).split("::", 1)[0]
|
||||||
|
if first not in internal_roots and first not in {"crate", "self", "super"}:
|
||||||
|
continue
|
||||||
|
|
||||||
candidates.append(
|
candidates.append(
|
||||||
Candidate(
|
Candidate(
|
||||||
code="RUST-IMPORT-201",
|
code="RUST-IMPORT-201",
|
||||||
path=relative,
|
path=relative,
|
||||||
line=line_number,
|
line=line_number,
|
||||||
message=(
|
message=(
|
||||||
f"long same-crate path `crate::{match.group(1)}::{match.group(2)}` "
|
f"non-canonical same-crate reference `{token}` for shared "
|
||||||
f"must use crate-root `crate::{alias}`"
|
f"{declaration.visibility} `{declaration.name}`; use `crate::{item.canonical_name}`"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def audit_target(workspace: pathlib.Path, target: Target) -> list[Candidate]:
|
||||||
|
"""Audit one independently compiled Rust crate target."""
|
||||||
|
|
||||||
|
declarations = _target_declarations(target)
|
||||||
|
exports = root_exports(target.root)
|
||||||
|
candidates: list[Candidate] = []
|
||||||
|
|
||||||
|
for declaration in declarations:
|
||||||
|
if declaration.path.resolve() == target.root and not declaration.module:
|
||||||
|
continue
|
||||||
|
required = declaration.visibility == "pub" or _external_usage(target, declaration)
|
||||||
|
if required and (declaration.module, declaration.name) not in exports:
|
||||||
|
reason = "public item" if declaration.visibility == "pub" else "crate-public item used outside its module"
|
||||||
|
candidates.append(
|
||||||
|
Candidate(
|
||||||
|
code="RUST-API-201",
|
||||||
|
path=declaration.path.relative_to(workspace).as_posix(),
|
||||||
|
line=declaration.line,
|
||||||
|
message=f"{reason} `{declaration.name}` requires an explicit crate-root re-export",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
candidates.extend(_canonical_reference_candidates(workspace, target, declarations, exports))
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
|
||||||
|
"""Audit every independently compiled target in one Cargo package."""
|
||||||
|
|
||||||
|
return [candidate for target in targets(crate) for candidate in audit_target(workspace, target)]
|
||||||
|
|
||||||
|
|
||||||
def audit_workspace(root: pathlib.Path) -> tuple[list[pathlib.Path], list[Candidate]]:
|
def audit_workspace(root: pathlib.Path) -> tuple[list[pathlib.Path], list[Candidate]]:
|
||||||
"""Return discovered crates and all façade candidates below one project root."""
|
"""Return discovered packages and all façade candidates below one project root."""
|
||||||
|
|
||||||
crates = crate_roots(root)
|
crates = crate_roots(root)
|
||||||
candidates = [candidate for crate in crates for candidate in audit_crate(root, crate)]
|
candidates = list({candidate for crate in crates for candidate in audit_crate(root, crate)})
|
||||||
candidates.sort(key=lambda candidate: (candidate.code, candidate.path, candidate.line, candidate.message))
|
candidates.sort(key=lambda candidate: (candidate.code, candidate.path, candidate.line, candidate.message))
|
||||||
return crates, candidates
|
return crates, candidates
|
||||||
|
|
||||||
@@ -264,27 +488,21 @@ def main() -> int:
|
|||||||
sys.stderr.write(f"Rust export completeness audit: invalid root: {workspace}\n")
|
sys.stderr.write(f"Rust export completeness audit: invalid root: {workspace}\n")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
try:
|
|
||||||
crates, candidates = audit_workspace(workspace)
|
crates, candidates = audit_workspace(workspace)
|
||||||
except (OSError, UnicodeError) as error:
|
|
||||||
sys.stderr.write(f"Rust export completeness audit: unable to read selected workspace: {error}\n")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
if not crates:
|
if not crates:
|
||||||
sys.stderr.write("Rust export completeness audit: no Rust crate root discovered\n")
|
sys.stderr.write(f"Rust export completeness audit: no Rust package found below {workspace}\n")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
counts[candidate.code] = counts.get(candidate.code, 0) + 1
|
counts[candidate.code] = counts.get(candidate.code, 0) + 1
|
||||||
|
|
||||||
sys.stdout.write(f"Rust export completeness audit: {len(candidates)} candidate(s) across {len(crates)} crate(s)\n")
|
sys.stdout.write(f"Rust export completeness audit: {len(candidates)} candidate(s) across {len(crates)} package(s)\n")
|
||||||
for code in sorted(counts):
|
for code in sorted(counts):
|
||||||
sys.stdout.write(f"{code}: {counts[code]}\n")
|
sys.stdout.write(f"{code}: {counts[code]}\n")
|
||||||
if not arguments.summary_only:
|
if not arguments.summary_only:
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
sys.stdout.write(f"{candidate.code} {candidate.path}:{candidate.line}: {candidate.message}\n")
|
sys.stdout.write(f"{candidate.code} {candidate.path}:{candidate.line}: {candidate.message}\n")
|
||||||
|
|
||||||
return 0 if arguments.report_only or not candidates else 1
|
return 0 if arguments.report_only or not candidates else 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: tests/test_rust_export_completeness.py
|
# file: tests/test_rust_export_completeness.py
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
"""Regression tests for the optional Rust crate-root façade audit."""
|
"""Regression tests for the optional Rust crate-root façade audit."""
|
||||||
|
|
||||||
@@ -105,6 +105,133 @@ class RustExportCompletenessTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(self.candidates(), [])
|
self.assertEqual(self.candidates(), [])
|
||||||
|
|
||||||
|
|
||||||
|
def test_bare_reference_in_declaration_module_is_rejected(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\npub use self::api::Thing;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": (
|
||||||
|
"pub struct Thing;\n"
|
||||||
|
"impl Thing {\n"
|
||||||
|
" pub fn make() -> Thing { return Thing; }\n"
|
||||||
|
"}\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"]
|
||||||
|
self.assertEqual(len(candidates), 2)
|
||||||
|
self.assertTrue(all("crate::Thing" in candidate.message for candidate in candidates))
|
||||||
|
|
||||||
|
def test_crate_root_reference_in_declaration_module_is_clean(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\npub use self::api::Thing;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": (
|
||||||
|
"pub struct Thing;\n"
|
||||||
|
"impl crate::Thing {\n"
|
||||||
|
" pub fn make() -> crate::Thing { return crate::Thing; }\n"
|
||||||
|
"}\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.candidates(), [])
|
||||||
|
|
||||||
|
def test_import_then_bare_reference_is_rejected(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\nmod consumer;\npub use self::api::Thing;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": "pub struct Thing;\n",
|
||||||
|
"src/consumer.rs": "use crate::Thing;\npub(crate) fn make() -> Thing { return Thing; }\n",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"]
|
||||||
|
self.assertEqual(len(candidates), 1)
|
||||||
|
|
||||||
|
def test_self_super_and_internal_module_paths_are_rejected(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\nmod consumer;\npub use self::api::Thing;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": "pub struct Thing;\n",
|
||||||
|
"src/consumer.rs": (
|
||||||
|
"pub(crate) fn a() -> crate::api::Thing { return crate::api::Thing; }\n"
|
||||||
|
"pub(crate) fn b() -> api::Thing { todo!() }\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"]
|
||||||
|
self.assertEqual(len(candidates), 2)
|
||||||
|
|
||||||
|
def test_attached_unit_test_visible_item_uses_crate_root(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\npub use self::api::Thing;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": (
|
||||||
|
"pub struct Thing;\n"
|
||||||
|
"#[cfg(test)]\n"
|
||||||
|
"#[path = \"../unit_tests/api_tests.rs\"]\n"
|
||||||
|
"mod tests;\n"
|
||||||
|
),
|
||||||
|
"unit_tests/api_tests.rs": "fn make() -> super::Thing { return super::Thing; }\n",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"]
|
||||||
|
self.assertEqual(len(candidates), 1)
|
||||||
|
|
||||||
|
def test_private_parent_item_in_attached_test_is_not_owned_by_this_audit(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": (
|
||||||
|
"fn helper() -> u8 { return 1; }\n"
|
||||||
|
"#[cfg(test)]\n"
|
||||||
|
"#[path = \"../unit_tests/api_tests.rs\"]\n"
|
||||||
|
"mod tests;\n"
|
||||||
|
),
|
||||||
|
"unit_tests/api_tests.rs": "fn value() -> u8 { return super::helper(); }\n",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.candidates(), [])
|
||||||
|
|
||||||
|
def test_external_qualified_same_named_item_is_not_misclassified(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\nmod consumer;\npub use self::api::Result;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": "pub struct Result;\n",
|
||||||
|
"src/consumer.rs": "pub(crate) fn value() -> external_crate::Result { todo!() }\n",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.candidates(), [])
|
||||||
|
|
||||||
|
def test_method_with_same_name_as_public_function_is_not_misclassified(self) -> None:
|
||||||
|
self.crate(
|
||||||
|
"crates/example-lib",
|
||||||
|
"mod api;\nmod consumer;\npub use self::api::run;\n",
|
||||||
|
{
|
||||||
|
"src/api.rs": "pub fn run() -> u8 { return 1; }\n",
|
||||||
|
"src/consumer.rs": "pub(crate) fn value(service: &Service) -> u8 { return service.run(); }\nstruct Service;\nimpl Service { fn run(&self) -> u8 { return 2; } }\n",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.candidates(), [])
|
||||||
|
|
||||||
|
def test_lib_and_main_targets_are_not_conflated(self) -> None:
|
||||||
|
crate = self.crate(
|
||||||
|
"crates/example-app",
|
||||||
|
"mod api;\npub use self::api::Thing;\n",
|
||||||
|
{"src/api.rs": "pub struct Thing;\n"},
|
||||||
|
)
|
||||||
|
(crate / "src/main.rs").write_text(
|
||||||
|
"fn main() { let _ = fixture::Thing; }\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
self.assertEqual(self.candidates(), [])
|
||||||
|
|
||||||
def test_cli_status_for_clean_violation_report_only_and_missing_crates(self) -> None:
|
def test_cli_status_for_clean_violation_report_only_and_missing_crates(self) -> None:
|
||||||
script = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "audit_rust_export_completeness.py"
|
script = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "audit_rust_export_completeness.py"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user