From 52bc30db2778268a52b5ced86f229a27b264e83b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:09:40 +0000 Subject: [PATCH 01/11] Initial plan From a63383476ae1988fbdef239a6cdefa8d17c6e8c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:14:07 +0000 Subject: [PATCH 02/11] Update type hint analyzer with type completeness exemptions Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 254 ++++++++++++++++++++- 1 file changed, 250 insertions(+), 4 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index e1dce96fd..66d739711 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -11,6 +11,18 @@ Based on the type completeness information from https://typing.python.org/en/latest/guides/libraries.html#type-completeness +The analyzer implements the following exemptions from type annotation requirements: + +1. Constants assigned simple literal values (e.g., RED = '#F00', MAX_TIMEOUT = 50) + - Named in ALL_CAPS or annotated with Final +2. Enum values within an Enum class +3. Type aliases (assignments to instantiable types) +4. self/cls parameters in methods +5. __init__ return types (always None) +6. Module-level symbols: __all__, __author__, __copyright__, __email__, + __license__, __title__, __uri__, __version__ +7. Class-level symbols: __class__, __dict__, __doc__, __module__, __slots__ + Usage: python type_hint_analyzer.py [--output-dir OUTPUT_DIR] @@ -28,6 +40,7 @@ from typing import Any, Dict, List, Tuple + def count_mypy_errors_by_submodule(pythainlp_dir: str) -> Dict[str, int]: """ Run mypy on each submodule and count errors. @@ -91,6 +104,7 @@ def __init__(self, filepath: str, module_path: str): self.current_class = None self.current_function = None self.module_level = True + self.current_class_bases = [] # Track base classes to detect Enum def is_private(self, name: str) -> bool: """Check if a name is private (starts with underscore).""" @@ -104,13 +118,19 @@ def is_public(self, name: str) -> bool: def check_function_type_hints( self, node: ast.FunctionDef - ) -> Tuple[str, int, int]: + ) -> Tuple[str, int, int, bool]: """ Check type hint completeness for a function. - Returns: (status, total_params, hinted_params) + + Returns: (status, total_params, hinted_params, has_return_hint) - status: "complete", "incomplete", "none" - total_params: number of parameters (excluding self/cls) - hinted_params: number of parameters with type hints + - has_return_hint: whether return type annotation is present + + According to type completeness guidelines: + - self/cls parameters don't require annotations + - __init__ methods don't require return type annotations """ # Count parameters (excluding self/cls) params = [] @@ -122,16 +142,35 @@ def check_function_type_hints( hinted_params = sum(1 for arg in params if arg.annotation is not None) has_return_hint = node.returns is not None + # __init__ methods don't need return type annotations + is_init = node.name == "__init__" + requires_return_hint = not is_init + # Determine status - if total_params == 0 and not has_return_hint: + if total_params == 0 and not requires_return_hint: + # No params and return type not required (e.g., __init__ with no params) + status = "complete" + elif total_params == 0 and requires_return_hint and not has_return_hint: + # No params but return type required and missing status = "none" elif total_params == 0 and has_return_hint: + # No params and has return type status = "complete" elif hinted_params == 0 and not has_return_hint: + # No hints at all status = "none" - elif hinted_params == total_params and has_return_hint: + elif requires_return_hint and ( + hinted_params == total_params and has_return_hint + ): + # All params and return type hinted + status = "complete" + elif ( + not requires_return_hint and hinted_params == total_params + ): + # All params hinted, return type not required (__init__) status = "complete" else: + # Partial hints status = "incomplete" return status, total_params, hinted_params, has_return_hint @@ -187,6 +226,170 @@ def _get_variable_name(self, target: ast.expr) -> str: else: return "unknown" + def _is_exempt_module_symbol(self, name: str) -> bool: + """ + Check if a symbol is exempt from type annotation requirements. + + According to type completeness guidelines, these module-level + symbols do not require type annotations: + __all__, __author__, __copyright__, __email__, __license__, + __title__, __uri__, __version__ + """ + exempt_symbols = { + "__all__", + "__author__", + "__copyright__", + "__email__", + "__license__", + "__title__", + "__uri__", + "__version__", + } + return name in exempt_symbols + + def _is_exempt_class_symbol(self, name: str) -> bool: + """ + Check if a class-level symbol is exempt from type annotations. + + According to type completeness guidelines, these class-level + symbols do not require type annotations: + __class__, __dict__, __doc__, __module__, __slots__ + """ + exempt_symbols = { + "__class__", + "__dict__", + "__doc__", + "__module__", + "__slots__", + } + return name in exempt_symbols + + def _is_simple_literal(self, value: ast.expr) -> bool: + """ + Check if a value is a simple literal. + + Simple literals include: strings, numbers, booleans, None, + and simple containers (list, tuple, dict, set) containing + only simple literals. + """ + if value is None: + return True + + # Direct literal types + if isinstance( + value, + (ast.Constant, ast.Num, ast.Str, ast.Bytes, ast.NameConstant), + ): + return True + + # Check for simple containers + if isinstance(value, (ast.List, ast.Tuple, ast.Set)): + return all(self._is_simple_literal(elt) for elt in value.elts) + + if isinstance(value, ast.Dict): + return all( + self._is_simple_literal(k) and self._is_simple_literal(v) + for k, v in zip(value.keys, value.values) + ) + + # Unary operations on literals (e.g., -1, +5) + if isinstance(value, ast.UnaryOp) and isinstance( + value.op, (ast.UAdd, ast.USub) + ): + return self._is_simple_literal(value.operand) + + return False + + def _is_constant_name(self, name: str) -> bool: + """ + Check if a variable name follows constant naming convention. + + Constants are typically named in ALL_CAPS or with Final annotation. + """ + # Check if name is in ALL_CAPS (with optional underscores) + if name.isupper() and not name.startswith("_"): + return True + return False + + def _has_final_annotation(self, annotation: ast.expr) -> bool: + """Check if an annotation uses Final.""" + if annotation is None: + return False + + # Check for Final + if isinstance(annotation, ast.Name) and annotation.id == "Final": + return True + + # Check for Final[...] (subscript) + if isinstance(annotation, ast.Subscript): + if isinstance(annotation.value, ast.Name): + return annotation.value.id == "Final" + elif isinstance(annotation.value, ast.Attribute): + return annotation.value.attr == "Final" + + # Check for typing.Final or typing_extensions.Final + if isinstance(annotation, ast.Attribute): + if annotation.attr == "Final": + return True + + return False + + def _is_in_enum_class(self) -> bool: + """Check if currently inside an Enum class.""" + if not self.current_class: + return False + + # Check if any base class looks like Enum + for base in self.current_class_bases: + if isinstance(base, ast.Name) and "Enum" in base.id: + return True + elif isinstance(base, ast.Attribute) and "Enum" in base.attr: + return True + + return False + + def _is_type_alias_without_annotation( + self, target: ast.expr, value: ast.expr + ) -> bool: + """ + Check if an assignment is a type alias without TypeAlias annotation. + + Type aliases are assignments where the value is an instantiable type. + Examples: MyType = dict[str, int], Foo = Optional[str] + """ + if value is None: + return False + + # Check for common type alias patterns + # 1. Subscripted types: List[str], Dict[int, str], etc. + if isinstance(value, ast.Subscript): + return True + + # 2. Union types with | operator (Python 3.10+) + if isinstance(value, ast.BinOp) and isinstance(value.op, ast.BitOr): + return True + + # 3. Type names that suggest type aliases + if isinstance(value, ast.Name): + # Common typing constructs + type_keywords = { + "Optional", + "Union", + "Callable", + "Type", + "Any", + "NoReturn", + } + if value.id in type_keywords: + return True + + # 4. Attribute access suggesting typing module usage + if isinstance(value, ast.Attribute): + # typing.Optional, typing.Union, etc. + return True + + return False + def visit_AnnAssign(self, node: ast.AnnAssign): """Visit annotated assignment (variable with type hint).""" # Skip if we're in a function/method body (local variables) @@ -264,6 +467,46 @@ def visit_Assign(self, node: ast.Assign): for target in node.targets: var_name = self._get_variable_name(target) + # Check for exemptions based on type completeness guidelines + # Module-level exempt symbols + if self.module_level and self._is_exempt_module_symbol(var_name): + self.generic_visit(node) + continue + + # Class-level exempt symbols + if ( + self.current_class is not None + and self.current_function is None + and self._is_exempt_class_symbol(var_name) + ): + self.generic_visit(node) + continue + + # Enum values - skip if we're in an Enum class + if ( + self.current_class is not None + and self.current_function is None + and self._is_in_enum_class() + ): + # Enum values don't require annotations + self.generic_visit(node) + continue + + # Constants with simple literal values don't require annotations + if self.module_level and self._is_constant_name(var_name): + if self._is_simple_literal(node.value): + # This is an exempt constant + self.generic_visit(node) + continue + + # Type aliases without annotation don't require annotations + if self.module_level and self._is_type_alias_without_annotation( + target, node.value + ): + # This is a type alias, doesn't need annotation + self.generic_visit(node) + continue + # Determine variable type and qualified name if self._is_instance_variable(target): var_type = "instance_variable" @@ -374,11 +617,14 @@ def visit_ClassDef(self, node: ast.ClassDef): # Visit class body (methods and class variables) old_class = self.current_class + old_class_bases = self.current_class_bases self.current_class = node.name + self.current_class_bases = node.bases # Track base classes for Enum detection old_module_level = self.module_level self.module_level = False self.generic_visit(node) self.current_class = old_class + self.current_class_bases = old_class_bases self.module_level = old_module_level From 44e4c559b6f6b841d817759fec2f897277504e57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:15:48 +0000 Subject: [PATCH 03/11] Fix deprecated AST nodes and update README with exemptions Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/README.md | 34 ++++++++++++++++++++++ build_tools/analysis/type_hint_analyzer.py | 7 ++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/build_tools/analysis/README.md b/build_tools/analysis/README.md index bb8c02408..c625cedb3 100644 --- a/build_tools/analysis/README.md +++ b/build_tools/analysis/README.md @@ -223,6 +223,34 @@ According to PEP 561 and the Python typing documentation, a library is considere 4. Generic types are properly parameterized 5. The library passes type checking with mypy in strict mode +**Exemptions from Type Annotation Requirements:** + +The analyzer implements the following exemptions as specified in the type completeness guidelines: + +1. **Constants with simple literal values** - Constants assigned simple literals don't require annotations + - Example: `MAX_VALUE = 100`, `DEFAULT_NAME = "test"` + - Must be named in ALL_CAPS or annotated with `Final` + - Complex values still require annotations: `COMPLEX_LIST: list = ["a", "b", {"key": "value"}]` + +2. **Enum values** - Values within an Enum class don't require annotations + - They automatically take on the type of the Enum class + +3. **Type aliases** - Type aliases don't require `TypeAlias` annotation + - Example: `MyType = dict[str, int]`, `Foo = Callable[[int], str]` + - These are recognized as type aliases by their assignment pattern + +4. **self and cls parameters** - Don't require explicit annotations + - Standard Python convention + +5. **__init__ return types** - Don't need annotation + - Always return None by definition + +6. **Special module-level symbols** - Don't require annotations + - `__all__`, `__author__`, `__copyright__`, `__email__`, `__license__`, `__title__`, `__uri__`, `__version__` + +7. **Special class-level symbols** - Don't require annotations + - `__class__`, `__dict__`, `__doc__`, `__module__`, `__slots__` + ### Analysis Categories **Type Hint Status:** @@ -235,12 +263,15 @@ According to PEP 561 and the Python typing documentation, a library is considere - **Functions/Methods:** Function signatures including parameters and return types - Excludes `self` and `cls` parameters from parameter counts + - `__init__` methods don't require return type annotations (always None) - Considers both parameters and return type for completeness - Tracks decorator usage (e.g., `@staticmethod`, `@lru_cache`) - **Class Variables:** Variables defined at class level - Distinguishes between annotated (`class_var: int = 10`) and non-annotated - Can include `ClassVar` type hints for class-specific attributes + - Special symbols like `__slots__` are exempt from annotation requirements + - Enum values are exempt (they inherit the Enum class type) - **Instance Variables:** Variables defined as instance attributes (e.g., `self.attr`) - Detected in `__init__` and other methods @@ -248,11 +279,14 @@ According to PEP 561 and the Python typing documentation, a library is considere - **Module Variables:** Variables defined at module level - Includes constants, configuration values, and exported names + - Constants with simple literal values (ALL_CAPS naming) are exempt + - Special symbols like `__all__`, `__version__` are exempt from annotation requirements - Important for library API clarity - **Type Aliases:** Type alias definitions (using TypeAlias annotation) - Modern syntax: `MyType: TypeAlias = dict[str, int]` - Also detects `typing.TypeAlias` and `typing_extensions.TypeAlias` + - Type aliases without explicit `TypeAlias` annotation are also recognized and exempt **Priority Levels:** diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 66d739711..37da9cf58 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -275,11 +275,8 @@ def _is_simple_literal(self, value: ast.expr) -> bool: if value is None: return True - # Direct literal types - if isinstance( - value, - (ast.Constant, ast.Num, ast.Str, ast.Bytes, ast.NameConstant), - ): + # Direct literal types (use ast.Constant for Python 3.8+) + if isinstance(value, ast.Constant): return True # Check for simple containers From ffa18029699410e1723c581460d4c5e8337fdad4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:17:17 +0000 Subject: [PATCH 04/11] Fix Ruff linting issues Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 37da9cf58..538c8e805 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -40,7 +40,6 @@ from typing import Any, Dict, List, Tuple - def count_mypy_errors_by_submodule(pythainlp_dir: str) -> Dict[str, int]: """ Run mypy on each submodule and count errors. @@ -64,8 +63,8 @@ def count_mypy_errors_by_submodule(pythainlp_dir: str) -> Dict[str, int]: submodule_path = os.path.join(pythainlp_dir, submodule) try: # Run mypy on the submodule - result = subprocess.run( - ["mypy", submodule_path, "--ignore-missing-imports"], + result = subprocess.run( # noqa: S603 + ["mypy", submodule_path, "--ignore-missing-imports"], # noqa: S607 capture_output=True, text=True, timeout=60, @@ -706,7 +705,7 @@ def count_references(qualified_name: str, all_files: List[str]) -> int: content = f.read() # Simple text search - not perfect but gives an approximation count += content.count(search_name) - except Exception: + except Exception: # noqa: S110 pass return count From fc88baf801370a778c5a4435919181c4973a54be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:19:51 +0000 Subject: [PATCH 05/11] Address code review feedback - fix constant detection and remove redundant calls Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 538c8e805..096c1b9f1 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -300,10 +300,13 @@ def _is_constant_name(self, name: str) -> bool: """ Check if a variable name follows constant naming convention. - Constants are typically named in ALL_CAPS or with Final annotation. + Constants are typically named in ALL_CAPS (with optional underscores). + This includes both public (MAX_VALUE) and private (_MAX_VALUE) constants. """ # Check if name is in ALL_CAPS (with optional underscores) - if name.isupper() and not name.startswith("_"): + # Remove leading underscores for the check + name_without_underscores = name.lstrip("_") + if name_without_underscores and name_without_underscores.isupper(): return True return False @@ -466,7 +469,6 @@ def visit_Assign(self, node: ast.Assign): # Check for exemptions based on type completeness guidelines # Module-level exempt symbols if self.module_level and self._is_exempt_module_symbol(var_name): - self.generic_visit(node) continue # Class-level exempt symbols @@ -475,7 +477,6 @@ def visit_Assign(self, node: ast.Assign): and self.current_function is None and self._is_exempt_class_symbol(var_name) ): - self.generic_visit(node) continue # Enum values - skip if we're in an Enum class @@ -485,14 +486,12 @@ def visit_Assign(self, node: ast.Assign): and self._is_in_enum_class() ): # Enum values don't require annotations - self.generic_visit(node) continue # Constants with simple literal values don't require annotations if self.module_level and self._is_constant_name(var_name): if self._is_simple_literal(node.value): # This is an exempt constant - self.generic_visit(node) continue # Type aliases without annotation don't require annotations @@ -500,7 +499,6 @@ def visit_Assign(self, node: ast.Assign): target, node.value ): # This is a type alias, doesn't need annotation - self.generic_visit(node) continue # Determine variable type and qualified name From 584a055ad8ecf2f58eed2489cb09efe0d2d2489d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:21:36 +0000 Subject: [PATCH 06/11] Apply code style improvements from review Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 096c1b9f1..af73a256c 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -306,9 +306,9 @@ def _is_constant_name(self, name: str) -> bool: # Check if name is in ALL_CAPS (with optional underscores) # Remove leading underscores for the check name_without_underscores = name.lstrip("_") - if name_without_underscores and name_without_underscores.isupper(): - return True - return False + return bool( + name_without_underscores and name_without_underscores.isupper() + ) def _has_final_annotation(self, annotation: ast.expr) -> bool: """Check if an annotation uses Final.""" @@ -489,10 +489,13 @@ def visit_Assign(self, node: ast.Assign): continue # Constants with simple literal values don't require annotations - if self.module_level and self._is_constant_name(var_name): - if self._is_simple_literal(node.value): - # This is an exempt constant - continue + if ( + self.module_level + and self._is_constant_name(var_name) + and self._is_simple_literal(node.value) + ): + # This is an exempt constant + continue # Type aliases without annotation don't require annotations if self.module_level and self._is_type_alias_without_annotation( From b8886efb6c0f44a2a2c795b5a2b60787596db676 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:23:46 +0000 Subject: [PATCH 07/11] Improve literal and type alias detection accuracy Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index af73a256c..4c3c7f117 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -275,8 +275,9 @@ def _is_simple_literal(self, value: ast.expr) -> bool: return True # Direct literal types (use ast.Constant for Python 3.8+) + # Only accept simple types: str, int, float, bool, None if isinstance(value, ast.Constant): - return True + return isinstance(value.value, (str, int, float, bool, type(None))) # Check for simple containers if isinstance(value, (ast.List, ast.Tuple, ast.Set)): @@ -355,6 +356,8 @@ def _is_type_alias_without_annotation( Type aliases are assignments where the value is an instantiable type. Examples: MyType = dict[str, int], Foo = Optional[str] + + This is conservative to avoid false positives. """ if value is None: return False @@ -382,10 +385,15 @@ def _is_type_alias_without_annotation( if value.id in type_keywords: return True - # 4. Attribute access suggesting typing module usage + # 4. Attribute access from typing module (typing.Something) if isinstance(value, ast.Attribute): - # typing.Optional, typing.Union, etc. - return True + # Only consider it a type alias if it's from typing/typing_extensions + if isinstance(value.value, ast.Name): + if value.value.id in ("typing", "typing_extensions"): + return True + # Or if the attribute name starts with uppercase (type convention) + if value.attr and value.attr[0].isupper(): + return True return False From 130f0731d00d49ccff3f7e0ab827f7f827274c53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:25:20 +0000 Subject: [PATCH 08/11] Add performance optimizations with module-level constants Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 4c3c7f117..6de86220f 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -39,6 +39,17 @@ from pathlib import Path from typing import Any, Dict, List, Tuple +# Constants for type checking +_NONE_TYPE = type(None) +_TYPE_ALIAS_KEYWORDS = { + "Optional", + "Union", + "Callable", + "Type", + "Any", + "NoReturn", +} + def count_mypy_errors_by_submodule(pythainlp_dir: str) -> Dict[str, int]: """ @@ -277,7 +288,7 @@ def _is_simple_literal(self, value: ast.expr) -> bool: # Direct literal types (use ast.Constant for Python 3.8+) # Only accept simple types: str, int, float, bool, None if isinstance(value, ast.Constant): - return isinstance(value.value, (str, int, float, bool, type(None))) + return isinstance(value.value, (str, int, float, bool, _NONE_TYPE)) # Check for simple containers if isinstance(value, (ast.List, ast.Tuple, ast.Set)): @@ -374,15 +385,7 @@ def _is_type_alias_without_annotation( # 3. Type names that suggest type aliases if isinstance(value, ast.Name): # Common typing constructs - type_keywords = { - "Optional", - "Union", - "Callable", - "Type", - "Any", - "NoReturn", - } - if value.id in type_keywords: + if value.id in _TYPE_ALIAS_KEYWORDS: return True # 4. Attribute access from typing module (typing.Something) From 679a80d9d50e04ac531ac828f3363e3c54e5118b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:48:10 +0000 Subject: [PATCH 09/11] Address PR review comments - fix documentation and remove unused code Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/README.md | 8 ++++-- build_tools/analysis/type_hint_analyzer.py | 32 +++------------------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/build_tools/analysis/README.md b/build_tools/analysis/README.md index c625cedb3..06fc5d557 100644 --- a/build_tools/analysis/README.md +++ b/build_tools/analysis/README.md @@ -228,9 +228,11 @@ According to PEP 561 and the Python typing documentation, a library is considere The analyzer implements the following exemptions as specified in the type completeness guidelines: 1. **Constants with simple literal values** - Constants assigned simple literals don't require annotations - - Example: `MAX_VALUE = 100`, `DEFAULT_NAME = "test"` - - Must be named in ALL_CAPS or annotated with `Final` - - Complex values still require annotations: `COMPLEX_LIST: list = ["a", "b", {"key": "value"}]` + - The variable name must be in ALL_CAPS (including private constants like `_MAX_VALUE`) + - The assigned value must be a "simple literal": str, int, float, bool, None, or containers (list, tuple, dict, set) composed only of such simple literals + - Example exempt constants: `MAX_VALUE = 100`, `DEFAULT_NAME = "test"`, `ALLOWED = ["a", "b", "c"]` + - Complex values containing non-simple literals still require annotations: `BYTES_VAL: bytes = b"data"` + - Note: The analyzer does not currently check for `Final` annotations when deciding exemptions 2. **Enum values** - Values within an Enum class don't require annotations - They automatically take on the type of the Enum class diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 6de86220f..b0950ec37 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -14,7 +14,8 @@ The analyzer implements the following exemptions from type annotation requirements: 1. Constants assigned simple literal values (e.g., RED = '#F00', MAX_TIMEOUT = 50) - - Named in ALL_CAPS or annotated with Final + - Named in ALL_CAPS (including private constants like _MAX_VALUE) + - Assigned a simple literal value (str, int, float, bool, None, or containers of these) 2. Enum values within an Enum class 3. Type aliases (assignments to instantiable types) 4. self/cls parameters in methods @@ -322,29 +323,6 @@ def _is_constant_name(self, name: str) -> bool: name_without_underscores and name_without_underscores.isupper() ) - def _has_final_annotation(self, annotation: ast.expr) -> bool: - """Check if an annotation uses Final.""" - if annotation is None: - return False - - # Check for Final - if isinstance(annotation, ast.Name) and annotation.id == "Final": - return True - - # Check for Final[...] (subscript) - if isinstance(annotation, ast.Subscript): - if isinstance(annotation.value, ast.Name): - return annotation.value.id == "Final" - elif isinstance(annotation.value, ast.Attribute): - return annotation.value.attr == "Final" - - # Check for typing.Final or typing_extensions.Final - if isinstance(annotation, ast.Attribute): - if annotation.attr == "Final": - return True - - return False - def _is_in_enum_class(self) -> bool: """Check if currently inside an Enum class.""" if not self.current_class: @@ -359,9 +337,7 @@ def _is_in_enum_class(self) -> bool: return False - def _is_type_alias_without_annotation( - self, target: ast.expr, value: ast.expr - ) -> bool: + def _is_type_alias_without_annotation(self, value: ast.expr) -> bool: """ Check if an assignment is a type alias without TypeAlias annotation. @@ -510,7 +486,7 @@ def visit_Assign(self, node: ast.Assign): # Type aliases without annotation don't require annotations if self.module_level and self._is_type_alias_without_annotation( - target, node.value + node.value ): # This is a type alias, doesn't need annotation continue From 28decadf42abcf5bf679ad8429f87282ca4ec4fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:04:01 +0000 Subject: [PATCH 10/11] Fix type alias detection and remove dead code per review Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 32 ++++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index b0950ec37..2807369d0 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -279,13 +279,12 @@ def _is_simple_literal(self, value: ast.expr) -> bool: """ Check if a value is a simple literal. + The value parameter must be an ast.expr node. + Simple literals include: strings, numbers, booleans, None, and simple containers (list, tuple, dict, set) containing only simple literals. """ - if value is None: - return True - # Direct literal types (use ast.Constant for Python 3.8+) # Only accept simple types: str, int, float, bool, None if isinstance(value, ast.Constant): @@ -344,15 +343,35 @@ def _is_type_alias_without_annotation(self, value: ast.expr) -> bool: Type aliases are assignments where the value is an instantiable type. Examples: MyType = dict[str, int], Foo = Optional[str] - This is conservative to avoid false positives. + This is conservative to avoid false positives like VALUE = mapping["key"]. """ if value is None: return False # Check for common type alias patterns # 1. Subscripted types: List[str], Dict[int, str], etc. + # Only if the base is a known type name or typing construct if isinstance(value, ast.Subscript): - return True + # Check if base is a Name and looks like a type + if isinstance(value.value, ast.Name): + base_name = value.value.id + # Common built-in types and typing constructs + known_types = { + "list", "dict", "set", "tuple", "frozenset", + "List", "Dict", "Set", "Tuple", "FrozenSet", + "Optional", "Union", "Callable", "Type", + "Sequence", "Mapping", "Iterable", "Iterator", + "Any", "Generic", "Protocol", "TypeVar", + } + # Base name starts with uppercase (type convention) or is a known type + if base_name in known_types or (base_name and base_name[0].isupper()): + return True + # Check if base is from typing module + elif isinstance(value.value, ast.Attribute): + if isinstance(value.value.value, ast.Name): + if value.value.value.id in ("typing", "typing_extensions"): + return True + return False # 2. Union types with | operator (Python 3.10+) if isinstance(value, ast.BinOp) and isinstance(value.op, ast.BitOr): @@ -370,9 +389,6 @@ def _is_type_alias_without_annotation(self, value: ast.expr) -> bool: if isinstance(value.value, ast.Name): if value.value.id in ("typing", "typing_extensions"): return True - # Or if the attribute name starts with uppercase (type convention) - if value.attr and value.attr[0].isupper(): - return True return False From 6d9e3d47ba2281f9520a3edb7488d49443573262 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:21:03 +0000 Subject: [PATCH 11/11] Fix bitwise OR detection to avoid false positives with flag operations Co-authored-by: bact <128572+bact@users.noreply.github.com> --- build_tools/analysis/type_hint_analyzer.py | 54 +++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/build_tools/analysis/type_hint_analyzer.py b/build_tools/analysis/type_hint_analyzer.py index 2807369d0..b0ffe98dc 100644 --- a/build_tools/analysis/type_hint_analyzer.py +++ b/build_tools/analysis/type_hint_analyzer.py @@ -336,6 +336,51 @@ def _is_in_enum_class(self) -> bool: return False + def _looks_like_type_expr(self, expr: ast.expr) -> bool: + """ + Check if an expression looks like a type expression. + + This helps distinguish type unions (int | str) from bitwise operations (1 | 2). + """ + # Constant None (Python 3.8+) + if isinstance(expr, ast.Constant) and expr.value is None: + return True + + # Name that's a known type keyword or built-in type + if isinstance(expr, ast.Name): + # Known typing constructs + if expr.id in _TYPE_ALIAS_KEYWORDS: + return True + # Known built-in types (lowercase) + known_types = { + "int", "str", "float", "bool", "bytes", + "list", "dict", "set", "tuple", "frozenset", + } + if expr.id in known_types: + return True + # PEP 585 style types (capitalized versions) + pep_585_types = { + "List", "Dict", "Set", "Tuple", "FrozenSet", + } + if expr.id in pep_585_types: + return True + + # Subscripted type + if isinstance(expr, ast.Subscript): + return self._looks_like_type_expr(expr.value) + + # Attribute from typing module + if isinstance(expr, ast.Attribute): + if isinstance(expr.value, ast.Name): + if expr.value.id in ("typing", "typing_extensions"): + return True + + # Nested union (already has | operator) + if isinstance(expr, ast.BinOp) and isinstance(expr.op, ast.BitOr): + return self._looks_like_type_expr(expr.left) and self._looks_like_type_expr(expr.right) + + return False + def _is_type_alias_without_annotation(self, value: ast.expr) -> bool: """ Check if an assignment is a type alias without TypeAlias annotation. @@ -343,7 +388,8 @@ def _is_type_alias_without_annotation(self, value: ast.expr) -> bool: Type aliases are assignments where the value is an instantiable type. Examples: MyType = dict[str, int], Foo = Optional[str] - This is conservative to avoid false positives like VALUE = mapping["key"]. + This is conservative to avoid false positives like VALUE = mapping["key"] + or bitwise operations like FLAGS = FLAG_A | FLAG_B. """ if value is None: return False @@ -374,8 +420,12 @@ def _is_type_alias_without_annotation(self, value: ast.expr) -> bool: return False # 2. Union types with | operator (Python 3.10+) + # Only if both operands look like type expressions if isinstance(value, ast.BinOp) and isinstance(value.op, ast.BitOr): - return True + # Check if both sides look like types (not bitwise flag operations) + if self._looks_like_type_expr(value.left) and self._looks_like_type_expr(value.right): + return True + return False # 3. Type names that suggest type aliases if isinstance(value, ast.Name):