Re: [docs] [PATCH v2 5/5] tools: Add check-confusables pre-commit hook

Niko Mauno <[email protected]> Tue, 28 Jul 2026 12:16:14 +0300
Newsgroups org.yoctoproject.lists.docs
Message-ID <[email protected]>

On 7/28/26 10:53 AM, Antonin Godard wrote:
> Hi,
>=20
> On Tue Jul 28, 2026 at 9:23 AM CEST, Niko Mauno via lists.yoctoproject.or=
g wrote:
>> From: Niko Mauno <[email protected]>
>>
>> Add a check-confusables script, in the same fashion as
>> check-glossaries, that scans the documentation .rst sources for
>> non-ASCII "confusable" characters (curly quotes, en/em dashes,
>> horizontal ellipsis, non-breaking and zero-width spaces, etc.) and
>> reports each occurrence with its location and suggested ASCII
>> replacement, exiting non-zero if any are found. This guards against
>> the class of breakage fixed in the preceding commit, e.g. curly quotes
>> causing recipe ParseErrors.
>>
>> Legitimate non-ASCII such as box-drawing characters used in directory
>> trees, accented letters in contributor names and CJK characters are
>> intentionally left untouched.
>>
>> Wire it up both as a local pre-commit hook and in the Makefile "checks"
>> target, alongside check-glossaries.
>>
>> Suggested-by: Quentin Schulz <[email protected]>
>> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
>> Signed-off-by: Niko Mauno <[email protected]>
>> ---
>>   .pre-commit-config.yaml               |  5 ++
>>   documentation/Makefile                |  1 +
>>   documentation/tools/check-confusables | 77 +++++++++++++++++++++++++++
>>   3 files changed, 83 insertions(+)
>>   create mode 100755 documentation/tools/check-confusables
>>
>> diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
>> index f2b73a481..d6008b609 100644
>> --- a/.pre-commit-config.yaml
>> +++ b/.pre-commit-config.yaml
>> @@ -6,3 +6,8 @@ repos:
>>           entry: ./documentation/tools/check-glossaries
>>           language: python
>>           pass_filenames: false
>> +      - id: check-confusables
>> +        name: Check for non-ASCII confusable characters
>> +        entry: ./documentation/tools/check-confusables
>> +        language: python
>> +        pass_filenames: false
>=20
> I think we should pass filenames and check only the modified files, no ne=
ed to
> checking the entire tree each time?
>=20
>> diff --git a/documentation/Makefile b/documentation/Makefile
>> index fe0574537..87a6f8a8b 100644
>> --- a/documentation/Makefile
>> +++ b/documentation/Makefile
>> @@ -37,6 +37,7 @@ clean:
>>  =20
>>   checks:
>>   	$(SOURCEDIR)/tools/check-glossaries --docs-dir $(SOURCEDIR)
>> +	$(SOURCEDIR)/tools/check-confusables --docs-dir $(SOURCEDIR)
>>  =20
>>   stylecheck:
>>   	vale sync
>> diff --git a/documentation/tools/check-confusables b/documentation/tools=
/check-confusables
>> new file mode 100755
>> index 000000000..f79ee046c
>> --- /dev/null
>> +++ b/documentation/tools/check-confusables
>> @@ -0,0 +1,77 @@
>> +#!/usr/bin/env python3
>> +
>> +import argparse
>> +import sys
>> +
>> +from pathlib import Path
>> +
>> +
>> +def parse_arguments() -> argparse.Namespace:
>> +    parser =3D argparse.ArgumentParser(
>> +        description=3D"Check documentation sources for non-ASCII typogr=
aphic "
>> +                    "characters that should be plain ASCII")
>> +
>> +    parser.add_argument("-d", "--docs-dir",
>> +                        type=3DPath,
>> +                        default=3DPath(__file__).resolve().parent.paren=
t,
>> +                        help=3D"Path to documentation/ directory in yoc=
to-docs")
>> +
>> +    return parser.parse_args()
>> +
>> +
>> +# Map of "confusable" characters that are frequently introduced by edit=
ors,
>> +# word processors or copy-pasting, to their plain ASCII replacement. Th=
ese
>> +# look almost identical to regular ASCII but break tooling, e.g. a curl=
y
>> +# quote in a recipe example causes:
>> +#
>> +#   ERROR: ParseError ...: unparsed line: 'RDEPENDS:${PN} =3D "foo"'
>=20
> I don't see the curly quote here
>=20
>> +#
>> +# Only these characters are flagged; legitimate non-ASCII such as box-d=
rawing
>> +# characters used in directory trees, accented letters in contributor n=
ames
>> +# and CJK characters are intentionally left alone.
>> +confusables =3D {
>> +    "=E2=80=98": "'",      # LEFT SINGLE QUOTATION MARK
>> +    "=E2=80=99": "'",      # RIGHT SINGLE QUOTATION MARK
>> +    "=E2=80=9C": '"',      # LEFT DOUBLE QUOTATION MARK
>> +    "=E2=80=9D": '"',      # RIGHT DOUBLE QUOTATION MARK
>> +    "=E2=80=B2": "'",      # PRIME
>> +    "=E2=80=B3": '"',      # DOUBLE PRIME
>> +    "=E2=80=93": "-",      # EN DASH
>> +    "=E2=80=94": "--",     # EM DASH
>> +    "=E2=80=90": "-",      # HYPHEN
>> +    "=E2=80=91": "-",      # NON-BREAKING HYPHEN
>> +    "=E2=88=92": "-",      # MINUS SIGN
>> +    "=E2=80=A6": "...",    # HORIZONTAL ELLIPSIS
>> +    "=C2=A0": " ",      # NO-BREAK SPACE
>=20
> Actually, since the `tree` command uses no-break spaces, I think it's OK =
to keep
> them otherwise we'd have to convert each tree output, which also might be
> confusing.
>=20
>> +    "=E2=80=AF": " ",      # NARROW NO-BREAK SPACE
>> +    "=E2=80=8B": "",       # ZERO WIDTH SPACE
>> +    "=EF=BB=BF": "",       # ZERO WIDTH NO-BREAK SPACE / BOM
>> +    "=C2=AD": "",       # SOFT HYPHEN
>> +}
>> +
>> +
>> +def main():
>> +
>> +    args =3D parse_arguments()
>> +    exit_code =3D 0
>> +
>> +    for rst_path in sorted(Path(args.docs_dir).rglob("*.rst")):
>> +        rel =3D rst_path.relative_to(args.docs_dir)
>> +
>> +        with open(rst_path, "r", encoding=3D"utf-8") as f:
>> +            for lineno, line in enumerate(f, start=3D1):
>> +                for col, char in enumerate(line, start=3D1):
>> +                    if char in confusables:
>> +                        replacement =3D confusables[char]
>> +                        hint =3D (f"'{replacement}'" if replacement
>> +                                else "(remove)")
>> +                        print(f"WARNING: {rel}:{lineno}:{col}: non-ASCI=
I "
>> +                              f"character U+{ord(char):04X} should be "
>> +                              f"replaced with {hint}")
>> +                        exit_code =3D 1
>> +
>> +    sys.exit(exit_code)
>> +
>> +
>> +if __name__ =3D=3D "__main__":
>> +    main()
>=20
>=20
> Antonin


Thank You Antonin, I have now submitted v3 which should address the=20
observations You pointed out above, plus adds Quentin's Reviewed-by line=20
in the bottom three commits which remained unchanged since v1, and also=20
leaves out conversion of "horizontal ellipsis" characters which seemed=20
to be used for denoting git hash truncation and therefore seemed to add=20
just noise to the fix commit.
-Niko