Skip to content

fix(entries): support symbol tokens in container operations - #30

Merged
kburov-sc merged 2 commits into
Snapchat:mainfrom
reginaldalfret:fix/support-symbol-tokens-in-prototype-chains
Oct 28, 2026
Merged

kburov-sc merged 2 commits into
Snapchat:mainfrom
reginaldalfret:fix/support-symbol-tokens-in-prototype-chains

Conversation

@reginaldalfret

@reginaldalfret reginaldalfret commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary of Changes

Enables full support for Symbol tokens across container operations by updating chainedForEach and chainedKeys in src/entries.ts.

Background & Defect Analysis

TokenType = string | number | symbol allows registering services using Symbol identifiers. However, the internal prototype-chain traversal helpers chainedForEach and chainedKeys previously used Object.keys(), which silently omits Symbol properties.

Because get() builds its internal resolution map via chainedForEach, on main no symbol-keyed service resolves through get() at all:

  • Container.providesValue(sym, v).get(sym) throws.
  • Using a symbol as a dependency throws during resolution.
  • Container.copy() and Container.provides(partialContainer) fail to preserve symbol-keyed services.

Fix Details

  1. src/entries.ts:

    • Replaced Object.keys(cur) with Reflect.ownKeys(cur) to discover both string and symbol keys.
    • Enforced enumerable property filtering with Object.prototype.propertyIsEnumerable.call(cur, k). This preserves the original behavior of ignoring non-enumerable properties (such as __esModule from CJS modules, Symbol.toStringTag from ESM namespace objects, and class prototype members).
    • Broadened seen deduplication set from Set<string> to Set<string | symbol>.
    • Updated doc comments to reflect enumerable string or symbol key traversal.
  2. src/Container.ts:

    • Broadened root and flat index signatures from Record<string, ...> to Record<string | symbol, ...>.
    • Preserved original formatting for providesClass and appendClass union types to ensure Prettier / CI styleguide compatibility.
  3. Regression Tests:

    • src/__tests__/Container.spec.ts: Added tests verifying that Symbol-keyed services resolve directly through get(), as dependencies, and across .copy().
    • src/__tests__/PartialContainer.spec.ts: Added tests verifying that Symbol-keyed services are preserved when merging a PartialContainer, and that non-enumerable keys (__esModule, Symbol.toStringTag) remain strictly ignored.

Test Results

  • npm test: 7/7 test suites passed, 140/140 tests passed.
  • Test coverage: 100% Statements, 100% Branches, 100% Functions, 100% Lines.
  • npm run lint: Clean (0 errors, 0 warnings).
  • npm run compile: Clean (0 errors).

@kburov-sc kburov-sc 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.

🤖 Note: This is an AI-generated first pass. I'll follow up with my own comments below.


Thanks for tracking this down! Fixing it in the shared chainedForEach helper is the right spot: get(), copy(), provides(partial), getTokens() and run(partial) are all repaired by one change. I ran both new tests against main, and both fail there, so they're real regression tests. Coverage stays at 100%.

The bug is broader than the description says. On main, no symbol-keyed service resolves through get() at all: Container.providesValue(sym, v).get(sym) throws, and so does using a symbol as a dependency, because get() builds its lookup map through chainedForEach. copy() and PartialContainer are just where it showed up. Consider updating the description so the changelog says symbol tokens now work in general.

Two things inline: a prettier failure that will block CI, and a suggestion to keep chainedForEach limited to enumerable keys.

Comment thread src/entries.ts Outdated
Comment on lines 20 to 23
const own = Reflect.ownKeys(cur);
for (let i = 0; i < own.length; i++) {
const k = own[i];
if (!seen.has(k)) {

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.

Reflect.ownKeys also returns non-enumerable keys, which Object.keys skipped. So this change makes the public Container / PartialContainer constructors register more than just symbols. One realistic case is passing a module namespace (import * as injectables from "./injectables"; new PartialContainer(injectables)). Its non-enumerable __esModule (CJS) or Symbol.toStringTag (ESM) key becomes a token, and then container.run(partial) throws Cannot read properties of undefined (reading 'map'). That works on main. Class instances also start registering constructor and their prototype methods.

Consider keeping the enumerable-only behavior and just adding symbols, which matches what object spread copies. Locally, all 138 tests pass with this and the symbol cases still resolve:

Suggested change
const own = Reflect.ownKeys(cur);
for (let i = 0; i < own.length; i++) {
const k = own[i];
if (!seen.has(k)) {
const own = Reflect.ownKeys(cur);
for (let i = 0; i < own.length; i++) {
const k = own[i];
if (!seen.has(k) && Object.prototype.propertyIsEnumerable.call(cur, k)) {

If you take this, the doc comment above only needs "enumerable string key" changed to "enumerable string or symbol key". A small test with a non-enumerable key would lock the behavior in.

Comment thread src/Container.ts Outdated
clsOrProvider:
| InjectableClass<any, any, readonly TokenType[]>
| (() => InjectableClass<any, any, readonly TokenType[]>)
InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>)

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.

This union reformat (and the same one in appendClass) looks unrelated to the fix, and it fails CI. The repo locks prettier 3.3.3 in package-lock.json. On this branch, npm run styleguide (the Build workflow's last step) flags src/Container.ts as unformatted, while main is clean. pnpm doesn't read package-lock.json, so it may have resolved a newer prettier. Restoring the leading | fixes it and matches PartialContainer.providesClass:

Suggested change
InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>)
| InjectableClass<any, any, readonly TokenType[]>
| (() => InjectableClass<any, any, readonly TokenType[]>)

Comment thread src/Container.ts Outdated
clsOrProvider:
| InjectableClass<any, any, readonly TokenType[]>
| (() => InjectableClass<any, any, readonly TokenType[]>)
InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>)

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.

Same prettier revert here:

Suggested change
InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>)
| InjectableClass<any, any, readonly TokenType[]>
| (() => InjectableClass<any, any, readonly TokenType[]>)

…ainedForEach

TokenType supports string | number | symbol, but chainedForEach and chainedKeys previously traversed prototype chains with Object.keys(), silently discarding symbol tokens. As a result, no symbol-keyed service resolved through get() or as dependencies, and operations such as Container.copy() and Container.provides(partialContainer) failed.

This replaces Object.keys() with Reflect.ownKeys() while enforcing enumerable-only keys via Object.prototype.propertyIsEnumerable to preserve filtering of non-enumerable properties (e.g. __esModule, Symbol.toStringTag), broadens the deduplication set to Set<string | symbol>, and adds comprehensive regression tests for symbol resolution and non-enumerable key exclusion.

Signed-off-by: reginaldalfret <reginaldalfret@gmail.com>
@reginaldalfret
reginaldalfret force-pushed the fix/support-symbol-tokens-in-prototype-chains branch from 83aee61 to 46b8edc Compare October 28, 2026 10:00
@reginaldalfret reginaldalfret changed the title fix(entries): support symbol tokens in chainedForEach and chainedKeys fix(entries): support symbol tokens in container operations Oct 28, 2026
@reginaldalfret

Copy link
Copy Markdown
Contributor Author

Hi @kburov-sc,

Thanks for the excellent review and catching the non-enumerable key edge case!

I've addressed all the feedback in the latest push:

  1. Enumerable-only constraint: Updated chainedForEach in src/entries.ts to check Object.prototype.propertyIsEnumerable.call(cur, k). This ensures non-enumerable keys (such as __esModule, Symbol.toStringTag, constructors, etc.) remain strictly ignored, preserving the exact semantics of Object.keys while extending support to enumerable symbols.
  2. Prettier formatting: Restored the leading | on the clsOrProvider union types in src/Container.ts so styleguide and Prettier pass cleanly without unrelated diffs.
  3. Regression tests:
    • Added test in src/__tests__/PartialContainer.spec.ts verifying that non-enumerable keys like __esModule and Symbol.toStringTag are excluded.
    • Added test in src/__tests__/Container.spec.ts asserting that symbol tokens resolve directly via get() and as dependencies.
  4. Updated PR description: Updated the PR title and description to reflect that symbol tokens now work generally across all container operations.

All 140 tests pass with 100% statement, branch, function, and line coverage, and lint and compile are clean!

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kburov-sc kburov-sc 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.

LGTM, thanks!

@kburov-sc
kburov-sc merged commit 52797ae into Snapchat:main Oct 28, 2026
1 check passed
@kburov-sc kburov-sc mentioned this pull request Oct 28, 2026
kburov-sc added a commit that referenced this pull request Oct 28, 2026
**Background**
Patch release for #30: symbol tokens now resolve through `get()`,
`copy()`, `provides(partial)`, `getTokens()` and `run()`.

**Change**
- Version bump only (1.2.0 → 1.2.1).

**Test Plan**
CI Build workflow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants