176 lines
7 KiB
Python
176 lines
7 KiB
Python
|
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||
|
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
|
||
|
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
|
||
|
|
||
|
"""All alphanumeric unicode character are allowed in Python but due
|
||
|
to similarities in how they look they can be confused.
|
||
|
|
||
|
See: https://peps.python.org/pep-0672/#confusing-features
|
||
|
|
||
|
The following checkers are intended to make users are aware of these issues.
|
||
|
"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
from astroid import nodes
|
||
|
|
||
|
from pylint import constants, interfaces, lint
|
||
|
from pylint.checkers import base_checker, utils
|
||
|
|
||
|
NON_ASCII_HELP = (
|
||
|
"Used when the name contains at least one non-ASCII unicode character. "
|
||
|
"See https://peps.python.org/pep-0672/#confusing-features"
|
||
|
" for a background why this could be bad. \n"
|
||
|
"If your programming guideline defines that you are programming in "
|
||
|
"English, then there should be no need for non ASCII characters in "
|
||
|
"Python Names. If not you can simply disable this check."
|
||
|
)
|
||
|
|
||
|
|
||
|
class NonAsciiNameChecker(base_checker.BaseChecker):
|
||
|
"""A strict name checker only allowing ASCII.
|
||
|
|
||
|
Note: This check only checks Names, so it ignores the content of
|
||
|
docstrings and comments!
|
||
|
"""
|
||
|
|
||
|
msgs = {
|
||
|
"C2401": (
|
||
|
'%s name "%s" contains a non-ASCII character, consider renaming it.',
|
||
|
"non-ascii-name",
|
||
|
NON_ASCII_HELP,
|
||
|
{"old_names": [("C0144", "old-non-ascii-name")]},
|
||
|
),
|
||
|
# First %s will always be "file"
|
||
|
"W2402": (
|
||
|
'%s name "%s" contains a non-ASCII character.',
|
||
|
"non-ascii-file-name",
|
||
|
(
|
||
|
# Some = PyCharm at the time of writing didn't display the non_ascii_name_loł
|
||
|
# files. That's also why this is a warning and not only a convention!
|
||
|
"Under python 3.5, PEP 3131 allows non-ascii identifiers, but not non-ascii file names."
|
||
|
"Since Python 3.5, even though Python supports UTF-8 files, some editors or tools "
|
||
|
"don't."
|
||
|
),
|
||
|
),
|
||
|
# First %s will always be "module"
|
||
|
"C2403": (
|
||
|
'%s name "%s" contains a non-ASCII character, use an ASCII-only alias for import.',
|
||
|
"non-ascii-module-import",
|
||
|
NON_ASCII_HELP,
|
||
|
),
|
||
|
}
|
||
|
|
||
|
name = "NonASCII-Checker"
|
||
|
|
||
|
def _check_name(self, node_type: str, name: str | None, node: nodes.NodeNG) -> None:
|
||
|
"""Check whether a name is using non-ASCII characters."""
|
||
|
|
||
|
if name is None:
|
||
|
# For some nodes i.e. *kwargs from a dict, the name will be empty
|
||
|
return
|
||
|
|
||
|
if not str(name).isascii():
|
||
|
type_label = constants.HUMAN_READABLE_TYPES[node_type]
|
||
|
args = (type_label.capitalize(), name)
|
||
|
|
||
|
msg = "non-ascii-name"
|
||
|
|
||
|
# Some node types have customized messages
|
||
|
if node_type == "file":
|
||
|
msg = "non-ascii-file-name"
|
||
|
elif node_type == "module":
|
||
|
msg = "non-ascii-module-import"
|
||
|
|
||
|
self.add_message(msg, node=node, args=args, confidence=interfaces.HIGH)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name", "non-ascii-file-name")
|
||
|
def visit_module(self, node: nodes.Module) -> None:
|
||
|
self._check_name("file", node.name.split(".")[-1], node)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name")
|
||
|
def visit_functiondef(
|
||
|
self, node: nodes.FunctionDef | nodes.AsyncFunctionDef
|
||
|
) -> None:
|
||
|
self._check_name("function", node.name, node)
|
||
|
|
||
|
# Check argument names
|
||
|
arguments = node.args
|
||
|
|
||
|
# Check position only arguments
|
||
|
if arguments.posonlyargs:
|
||
|
for pos_only_arg in arguments.posonlyargs:
|
||
|
self._check_name("argument", pos_only_arg.name, pos_only_arg)
|
||
|
|
||
|
# Check "normal" arguments
|
||
|
if arguments.args:
|
||
|
for arg in arguments.args:
|
||
|
self._check_name("argument", arg.name, arg)
|
||
|
|
||
|
# Check key word only arguments
|
||
|
if arguments.kwonlyargs:
|
||
|
for kwarg in arguments.kwonlyargs:
|
||
|
self._check_name("argument", kwarg.name, kwarg)
|
||
|
|
||
|
visit_asyncfunctiondef = visit_functiondef
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name")
|
||
|
def visit_global(self, node: nodes.Global) -> None:
|
||
|
for name in node.names:
|
||
|
self._check_name("const", name, node)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name")
|
||
|
def visit_assignname(self, node: nodes.AssignName) -> None:
|
||
|
"""Check module level assigned names."""
|
||
|
# The NameChecker from which this Checker originates knows a lot of different
|
||
|
# versions of variables, i.e. constants, inline variables etc.
|
||
|
# To simplify we use only `variable` here, as we don't need to apply different
|
||
|
# rules to different types of variables.
|
||
|
frame = node.frame()
|
||
|
|
||
|
if isinstance(frame, nodes.FunctionDef):
|
||
|
if node.parent in frame.body:
|
||
|
# Only perform the check if the assignment was done in within the body
|
||
|
# of the function (and not the function parameter definition
|
||
|
# (will be handled in visit_functiondef)
|
||
|
# or within a decorator (handled in visit_call)
|
||
|
self._check_name("variable", node.name, node)
|
||
|
elif isinstance(frame, nodes.ClassDef):
|
||
|
self._check_name("attr", node.name, node)
|
||
|
else:
|
||
|
# Possibilities here:
|
||
|
# - isinstance(node.assign_type(), nodes.Comprehension) == inlinevar
|
||
|
# - isinstance(frame, nodes.Module) == variable (constant?)
|
||
|
# - some other kind of assignment missed but still most likely a variable
|
||
|
self._check_name("variable", node.name, node)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name")
|
||
|
def visit_classdef(self, node: nodes.ClassDef) -> None:
|
||
|
self._check_name("class", node.name, node)
|
||
|
for attr, anodes in node.instance_attrs.items():
|
||
|
if not any(node.instance_attr_ancestors(attr)):
|
||
|
self._check_name("attr", attr, anodes[0])
|
||
|
|
||
|
def _check_module_import(self, node: nodes.ImportFrom | nodes.Import) -> None:
|
||
|
for module_name, alias in node.names:
|
||
|
name = alias or module_name
|
||
|
self._check_name("module", name, node)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name", "non-ascii-module-import")
|
||
|
def visit_import(self, node: nodes.Import) -> None:
|
||
|
self._check_module_import(node)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name", "non-ascii-module-import")
|
||
|
def visit_importfrom(self, node: nodes.ImportFrom) -> None:
|
||
|
self._check_module_import(node)
|
||
|
|
||
|
@utils.only_required_for_messages("non-ascii-name")
|
||
|
def visit_call(self, node: nodes.Call) -> None:
|
||
|
"""Check if the used keyword args are correct."""
|
||
|
for keyword in node.keywords:
|
||
|
self._check_name("argument", keyword.arg, keyword)
|
||
|
|
||
|
|
||
|
def register(linter: lint.PyLinter) -> None:
|
||
|
linter.register_checker(NonAsciiNameChecker(linter))
|