[IMP] hygiène : lire aussi les commentaires Go

L'outil n'inspectait que « # » : sondé avec des motifs interdits, il restait
muet sur un « .go » et sortait 0. Du Go étant entré dans le dépôt, ses
commentaires n'étaient relus par personne d'autre que leur auteur, alors que
la convention vaut pour tout commentaire.

Trois pièges, et le premier est celui qui compte : « // » ouvre un
commentaire SAUF dans une chaîne, et « https:// » en porte deux. La chaîne
brute entre accents graves n'échappe rien, et « /* … */ » couvre plusieurs
lignes. Vérifié : 8 cas neufs, dont l'URL qui ne doit rien ouvrir.

--- EN ---

The tool only inspected « # »: probed with forbidden patterns, it stayed
silent on a « .go » and exited 0. Now that Go has entered the repository, its
comments were read by nobody but their author, while the convention covers
every comment.

Three traps, and the first is the one that matters: « // » opens a comment
EXCEPT inside a string, and « https:// » carries two of them. The raw string
between backticks escapes nothing, and « /* … */ » spans several lines.
Checked: 8 new cases, including the URL that must open nothing.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-14 14:41:19 -04:00
parent 60d35afdf4
commit c98c69db1c
2 changed files with 122 additions and 1 deletions

View file

@ -68,7 +68,7 @@ except Exception: # pragma: no cover - repli si i18n indisponible
return key
SUFFIXES = (".py", ".sh", ".bash")
SUFFIXES = (".py", ".sh", ".bash", ".go")
# Ce qui vient d'ailleurs ou n'est pas du source : le dépôt ne le réécrit pas.
EXCLUS = (
@ -308,10 +308,70 @@ def blocs_shell(source):
return _regroupe(commentaires)
def blocs_go(source):
"""Les commentaires d'un fichier Go, les consécutifs regroupés.
Trois pièges, et le premier est celui qui compte : « // » ouvre un
commentaire SAUF dans une chaîne — et « https:// » en porte deux. Go a
trois formes de chaîne, dont la brute entre accents graves, où la barre
oblique inverse n'échappe rien. Les commentaires de bloc « /* … */ »
couvrent plusieurs lignes, chacune comptant pour ce qu'elle dit.
"""
commentaires = []
en_bloc = False
for numero, ligne in enumerate(source.split("\n"), start=1):
if en_bloc:
fin = ligne.find("*/")
texte = (ligne if fin < 0 else ligne[:fin]).strip()
if texte:
commentaires.append((numero, texte.lstrip("*").strip()))
if fin >= 0:
en_bloc = False
continue
quote = None
precedent = ""
index = 0
while index < len(ligne):
caractere = ligne[index]
if precedent == "\\" and quote in ('"', "'"):
# La chaîne brute ignore l'échappement : seules les deux
# autres formes le connaissent.
precedent = ""
index += 1
continue
if quote:
if caractere == quote:
quote = None
elif caractere in "\"'`":
quote = caractere
elif caractere == "/" and ligne[index : index + 2] == "//":
texte = ligne[index:].lstrip("/").strip()
if texte:
commentaires.append((numero, texte))
break
elif caractere == "/" and ligne[index : index + 2] == "/*":
reste = ligne[index + 2 :]
fin = reste.find("*/")
texte = (reste if fin < 0 else reste[:fin]).strip()
if texte:
commentaires.append((numero, texte))
if fin < 0:
en_bloc = True
break
index += 2 + fin + 2
precedent = ""
continue
precedent = caractere
index += 1
return _regroupe(commentaires)
def blocs(chemin, source):
"""Les commentaires d'un fichier, selon son suffixe."""
if chemin.endswith(".py"):
return blocs_python(source)
if chemin.endswith(".go"):
return blocs_go(source)
return blocs_shell(source)

View file

@ -134,6 +134,67 @@ class TestLeTemoignage(unittest.TestCase):
self.assertEqual(2, len({t[1].lower() for t in trouves}))
class TestLeGo(unittest.TestCase):
"""Go est entré dans le dépôt, et l'outil ne le lisait pas.
Sondé avec des motifs interdits, il restait muet sur un « .go » et sortait
0 : les commentaires du cache de téléchargement n'ont donc jamais été
relus par personne d'autre que leur auteur.
"""
def texte(self, source):
"""Le texte recollé de tous les blocs. Un bloc est un dict :
« line », « text », « lines », « offsets »."""
return " ".join(b["text"] for b in hygiene.blocs_go(source))
def test_un_commentaire_de_ligne(self):
trouve = hygiene.inspect("x.go", "// Vécu : la panne\npackage main\n")
self.assertTrue(trouve, "un commentaire Go n'est pas inspecté")
def test_une_url_en_chaine_nouvre_rien(self):
"""« https:// » porte deux barres obliques : c'est LE piège du Go."""
source = 'package main\n\nconst a = "https://vecu.example/hier"\n'
self.assertEqual(hygiene.blocs_go(source), [])
def test_une_chaine_brute_nouvre_rien(self):
"""L'accent grave délimite une chaîne où rien ne s'échappe."""
source = (
"package main\n\nconst a = `https://mesure.example/nous avons`\n"
)
self.assertEqual(hygiene.blocs_go(source), [])
def test_un_commentaire_apres_une_chaine(self):
source = (
'package main\n\nconst a = "https://x.example" // Vécu : ici\n'
)
trouve = hygiene.recits(self.texte(source))
self.assertTrue(trouve, "le commentaire qui suit une URL est perdu")
def test_un_bloc_sur_plusieurs_lignes(self):
source = (
"package main\n\n/*\nVécu : la semaine où\nnous avons vu.\n*/\n"
)
self.assertTrue(
hygiene.blocs_go(source), "un commentaire /* */ n'est pas lu"
)
texte = self.texte(source)
self.assertIn("Vécu", texte)
self.assertIn("nous avons", texte)
def test_un_bloc_sur_une_seule_ligne(self):
source = "package main\n\nvar x = 1 /* Vécu : ici */\nvar y = 2\n"
self.assertTrue(hygiene.blocs_go(source))
self.assertIn("Vécu", self.texte(source))
def test_le_code_apres_un_bloc_ferme_est_relu(self):
"""Un « // » qui suit un bloc fermé sur la même ligne compte encore."""
source = "package main\n\nvar x = 1 /* rien */ // Vécu : là\n"
self.assertIn("Vécu", self.texte(source))
def test_le_suffixe_est_balaye(self):
self.assertIn(".go", hygiene.SUFFIXES)
class TestLesIdentifiants(unittest.TestCase):
def test_un_courriel(self):
self.assertIn(