| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- # 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))
|