Skip to content

fix(integrations): refuse scaffold keys that shadow a module or name a keyword - #4540

Open
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/scaffold-reject-colliding-keys
Open

jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/scaffold-reject-colliding-keys

Conversation

@jawwad-ali

@jawwad-ali jawwad-ali commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Problem

scaffold_integration validates the key's shape (_KEY_RE, lowercase kebab-case) but never checks what the derived package name would collide with. Two ordinary keys produce broken output.

1. A key that shadows one of this package's own modules

The scaffold creates a packageintegrations/<key>/__init__.py — and in Python a package shadows a same-named module in the same directory:

both 'base.py' and 'base/' present -> import demopkg.base resolves to: scaffolded package base/

Every integration in the repo does from ..base import MarkdownIntegration / SkillsIntegration, so scaffolding a key named base silently redirects all of them to the empty scaffold.

The existing guard cannot catch this — it only checks <key>/__init__.py and the test file:

existing = [path for path in (integration_file, test_file) if path.exists()]

integrations/base.py is never consulted. Currently accepted and collidable: base, catalog, manifest.

2. A key that is a reserved Python keyword

integrations/class/ cannot be named by any import statement, so the generated package is unreachable and its generated test (from specify_cli.integrations.class import ClassIntegration) is a SyntaxError. Currently accepted: class, import, return, lambda, …

Reproduction on current main (c173bf1)

Every one of these passes _clean_key:

_clean_key('base'    ) -> ACCEPTED 'base'
_clean_key('catalog' ) -> ACCEPTED 'catalog'
_clean_key('manifest') -> ACCEPTED 'manifest'
_clean_key('class'   ) -> ACCEPTED 'class'
_clean_key('import'  ) -> ACCEPTED 'import'

Fix

Refuse both up front, before anything is written:

base       -> refused (collision)
catalog    -> refused (collision)
manifest   -> refused (collision)
class      -> refused (keyword)
import     -> refused (keyword)
match      -> ACCEPTED
my-agent   -> ACCEPTED

Soft keywords are deliberately allowed

My first cut rejected soft keywords too. I checked rather than assumed, and that would have been wrong — soft keywords are contextual and import match is perfectly valid:

soft keyword 'match' -> importable? YES   iskeyword=False issoftkeyword=True
soft keyword 'case'  -> importable? YES   iskeyword=False issoftkeyword=True

So the guard uses keyword.iskeyword only, and match / case remain usable keys. (_ is not a usable key at all: _clean_key's lowercase kebab-case pattern requires a leading letter and rejects it before this guard runs.)

Verification

  • Fail-before / pass-after: 7 new-vs-baseline failures with the source reverted to upstream/main25 passed, 1 skipped with the fix.
  • The shadowing test asserts the real module is left byte-identical and that no package was created beside it — not merely that an error was raised.
  • A third test pins that soft keywords and ordinary keys still scaffold successfully, so the guard cannot start refusing valid input.
  • uvx ruff@0.15.0 check src tests → clean

Behaviour change, disclosed: five key values that previously scaffolded now raise. All five produced output that was broken on arrival — an unimportable package, or one that shadows a module the whole package depends on.


Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main.

🤖 Generated with Claude Code

…a keyword

`scaffold_integration` validates the key's *shape* (`_KEY_RE`, kebab-case) but
never checks what the derived package name would collide with. Two ordinary
keys produce broken output:

1. A key matching one of this package's own modules. The scaffold creates a
   PACKAGE, `integrations/<key>/`, and a package shadows a same-named module in
   the same directory:

       both 'base.py' and 'base/' present
       -> import demopkg.base resolves to: scaffolded package base/

   Every integration does `from ..base import MarkdownIntegration`, so
   scaffolding a key named `base` would silently redirect all of them to the
   empty scaffold. The existing-file guard cannot catch this: it only checks
   `<key>/__init__.py`, never `<key>.py`. `base`, `catalog` and `manifest` are
   all currently accepted.

2. A reserved Python keyword. `integrations/class/` cannot be named by any
   import statement, so the generated package is unreachable. `class`, `import`,
   `return` and `lambda` are all currently accepted.

Both are refused up front now, before anything is written.

Soft keywords are deliberately NOT rejected -- they are contextual and
`import match` is valid, so `match`, `case` and `_` remain usable keys. This
was verified rather than assumed:

    soft keyword 'match' -> importable? YES   iskeyword=False issoftkeyword=True

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali requested a review from mnriem as a code owner September 11, 2026 17:47
@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Sep 12, 2026
@mnriem
mnriem requested a balanced review from Copilot September 18, 2026 12:08

Copilot AI left a comment

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.

🟢 Approval recommended

The implementation and regression coverage are sound; only a minor documentation correction remains.

Pull request overview

Prevents integration scaffolds from creating invalid or module-shadowing Python packages.

Changes:

  • Rejects Python keyword keys and module-name collisions.
  • Adds regression and valid-key coverage.
File summaries
File Description
tests/integrations/test_integration_scaffold.py Tests rejected collisions, keywords, and accepted keys.
src/specify_cli/integration_scaffold.py Adds keyword and module-collision validation.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/specify_cli/integration_scaffold.py Outdated

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address Copilot feedback and resolve conflicts

…ffold key

Addresses review feedback. The keyword-guard comment said soft keywords
"(``match``, ``case``, ``_``)" are deliberately not rejected. `_` never
reaches this guard: `_clean_key` runs first, and its lowercase kebab-case
pattern (`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`) requires a leading letter, so it
rejects `_` outright:

    _clean_key('_'    ) -> REJECTED by the kebab-case pattern
    _clean_key('match') -> ACCEPTED
    _clean_key('case' ) -> ACCEPTED

The comment now names only the soft keywords `_clean_key` actually admits. The
PR description is corrected to match. Comment-only; the acceptance test already
covered just `match` and `case`, so it needed no change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

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

Labels

triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants