From c98c69db1cf43c3dbcb6b40cfc6b6e5c767c3096 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 14 Sep 2026 14:41:19 -0400 Subject: [PATCH] =?UTF-8?q?[IMP]=20hygi=C3=A8ne=20:=20lire=20aussi=20les?= =?UTF-8?q?=20commentaires=20Go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- script/analyse/check_comment_hygiene.py | 62 ++++++++++++++++++++++++- test/test_check_comment_hygiene.py | 61 ++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/script/analyse/check_comment_hygiene.py b/script/analyse/check_comment_hygiene.py index a2e8f8d..d1c1795 100755 --- a/script/analyse/check_comment_hygiene.py +++ b/script/analyse/check_comment_hygiene.py @@ -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) diff --git a/test/test_check_comment_hygiene.py b/test/test_check_comment_hygiene.py index ae248a3..84d5173 100644 --- a/test/test_check_comment_hygiene.py +++ b/test/test_check_comment_hygiene.py @@ -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(