Repository navigation
Conversation
kburov-sc
left a comment
There was a problem hiding this comment.
🤖 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.
| const own = Reflect.ownKeys(cur); | ||
| for (let i = 0; i < own.length; i++) { | ||
| const k = own[i]; | ||
| if (!seen.has(k)) { |
There was a problem hiding this comment.
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:
| 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.
| clsOrProvider: | ||
| | InjectableClass<any, any, readonly TokenType[]> | ||
| | (() => InjectableClass<any, any, readonly TokenType[]>) | ||
| InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>) |
There was a problem hiding this comment.
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:
| InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>) | |
| | InjectableClass<any, any, readonly TokenType[]> | |
| | (() => InjectableClass<any, any, readonly TokenType[]>) |
| clsOrProvider: | ||
| | InjectableClass<any, any, readonly TokenType[]> | ||
| | (() => InjectableClass<any, any, readonly TokenType[]>) | ||
| InjectableClass<any, any, readonly TokenType[]> | (() => InjectableClass<any, any, readonly TokenType[]>) |
There was a problem hiding this comment.
Same prettier revert here:
| 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>
83aee61 to
46b8edc
Compare
|
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:
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>
**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>
Summary of Changes
Enables full support for
Symboltokens across container operations by updatingchainedForEachandchainedKeysinsrc/entries.ts.Background & Defect Analysis
TokenType = string | number | symbolallows registering services usingSymbolidentifiers. However, the internal prototype-chain traversal helperschainedForEachandchainedKeyspreviously usedObject.keys(), which silently omitsSymbolproperties.Because
get()builds its internal resolution map viachainedForEach, onmainno symbol-keyed service resolves throughget()at all:Container.providesValue(sym, v).get(sym)throws.Container.copy()andContainer.provides(partialContainer)fail to preserve symbol-keyed services.Fix Details
src/entries.ts:Object.keys(cur)withReflect.ownKeys(cur)to discover both string and symbol keys.Object.prototype.propertyIsEnumerable.call(cur, k). This preserves the original behavior of ignoring non-enumerable properties (such as__esModulefrom CJS modules,Symbol.toStringTagfrom ESM namespace objects, and class prototype members).seendeduplication set fromSet<string>toSet<string | symbol>.src/Container.ts:rootandflatindex signatures fromRecord<string, ...>toRecord<string | symbol, ...>.providesClassandappendClassunion types to ensure Prettier / CI styleguide compatibility.Regression Tests:
src/__tests__/Container.spec.ts: Added tests verifying that Symbol-keyed services resolve directly throughget(), as dependencies, and across.copy().src/__tests__/PartialContainer.spec.ts: Added tests verifying that Symbol-keyed services are preserved when merging aPartialContainer, and that non-enumerable keys (__esModule,Symbol.toStringTag) remain strictly ignored.Test Results
npm test: 7/7 test suites passed, 140/140 tests passed.npm run lint: Clean (0 errors, 0 warnings).npm run compile: Clean (0 errors).