Skip to content

feat(pre-commit): add internal-import alias check with auto-fix#49

Open
dengqiaoyu wants to merge 1 commit into
apple:mainfrom
dengqiaoyu:u/qiaoyu_deng/private-import-check
Open

feat(pre-commit): add internal-import alias check with auto-fix#49
dengqiaoyu wants to merge 1 commit into
apple:mainfrom
dengqiaoyu:u/qiaoyu_deng/private-import-check

Conversation

@dengqiaoyu

Copy link
Copy Markdown
Contributor
  • Add an AST-based hook enforcing code style guide §3.3: public modules must _-alias symbols imported from private modules; private modules must not
  • Auto-fix offending import statements and rename in-file references, refusing the fix only when the bound name is shadowed
  • Register the hook in .pre-commit-config.yaml and document it under §3.3 "Enforcement"
  • Apply across src/coreai_opt/, normalizing 27 modules with missing or redundant _ aliases

- Add an AST-based hook enforcing code style guide §3.3: public modules must `_`-alias symbols imported from private modules; private modules must not
- Auto-fix offending import statements and rename in-file references, refusing the fix only when the bound name is shadowed
- Register the hook in `.pre-commit-config.yaml` and document it under §3.3 "Enforcement"
- Apply across `src/coreai_opt/`, normalizing 27 modules with missing or redundant `_` aliases
@dengqiaoyu
dengqiaoyu requested review from crowbat and guru-desh July 20, 2026 19:16
@dengqiaoyu dengqiaoyu added documentation Improvements or additions to documentation enhancement New feature or request labels Jul 20, 2026
yield from ast.walk(stmt)


def _locally_binds_name(node: ast.AST, name: str) -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 comments on this function:

  1. It looks like it misses out on the case where t in node.targets itself is a tuple/list and when node is a comprehension. Updating the checks to use _is_named_target could dedupe some logic and fix these issues at the same time:
def _locally_binds_name(node: ast.AST, name: str) -> bool:
    if isinstance(node, ast.Assign):
        return any(_is_named_target(t, name) for t in node.targets)
    if isinstance(node, (ast.AnnAssign, ast.AugAssign, ast.NamedExpr, ast.For,
ast.AsyncFor)):
        return _is_named_target(node.target, name)
    if isinstance(node, (ast.With, ast.AsyncWith)):
        return any(
            item.optional_vars is not None and
_is_named_target(item.optional_vars, name)
            for item in node.items
        )
    if isinstance(node, ast.comprehension):
        return _is_named_target(node.target, name)
    return False
  1. This function and _binds_assignment_target share very similar logic, could they be combined into one?


def _node_span(node: _Positioned, line_starts: list[int]) -> tuple[int, int]:
"""Convert an AST node's (lineno, col_offset, end_lineno, end_col_offset) to byte offsets."""
start = line_starts[node.lineno - 1] + node.col_offset

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat a corner case, but if the code has some character that is non-ASCII that uses more than one byte, the logic here can result in a malformed update:

# Before the fix
from temp_pkg._priv import helper

def use():
    café = ...
    return café + helper()

# After the fix
from temp_pkg._priv import helper as _helper
  
def use():
    café = ...
    return café + h_helper)

"""
violations: list[Violation] = []

for node in ast.walk(tree):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should we also check for and flag plain imports of private packages? Like import pkg._private
  2. Looks like we are missing checks for relative imports (from ._priv import x)

"""
original = alias.name
current_alias = alias.asname
if original.startswith("_") or current_alias not in (None, original, "_" + original):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another corner case, this currently does not handle the case if someone names things with a custom alias (from a._b import c as d)

)


def _statement_edits(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an import line has a comment at the end, looks like the comment would be removed as part of the rename

Comment thread .pre-commit-config.yaml
symbols imported from private modules with a `_` prefix; private
modules must not. Auto-fixes the import statement and renames
references in the same file.
entry: python scripts/pre_commit/check_internal_import_aliases.py --fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it seems like there are a lot of potential corner cases associated with automatically fixing the code. What do you think about making --fix opt-in instead where the user manually runs it if they want to have things be automatically fixed?

We could have the pre-commit hook only do the detection part of it to fail the commit if it finds any transgressions. I'm thinking that if any automatic fixes are made which break the code, other pre-commit hooks may also have run (linting, line too long, etc.) which would conflate the changes if the user needs to revert the code back to before this hook took action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants