Relocate parts of makeImmutable to lib/util.js#875
Conversation
|
Reference #871 |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR refactors object immutability handling by introducing Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@lib/config.js`:
- Around line 116-128: The internal call from Config.get to make the config
immutable is triggering the user-facing deprecation in Util.errorOnce; change
the internal path so it does not fire the deprecation by adding an
internal/suppress flag or a private method on ConfigUtils (e.g.
ConfigUtils.makeImmutable(suppressDeprecation=true) or
ConfigUtils._makeImmutable) and update the call in get() (the invocation
this.util.makeImmutable()) to use the internal/suppress variant; then update
ConfigUtils.makeImmutable to only call Util.errorOnce("MAKEIMMUTABLE", ...) when
the suppress flag is false (and remove the object === undefined branch as
noted). Ensure references: get, LOAD_SYMBOL/initParam, this.util.makeImmutable,
ConfigUtils.makeImmutable, and Util.errorOnce are adjusted accordingly.
In `@lib/util.js`:
- Around line 137-208: makeImmutable currently unconditionally calls
Object.defineProperty for RawConfig and array branches causing a "Cannot
redefine property" on repeated calls; update the RawConfig handling (value
instanceof RawConfig) and the Array.isArray branch to first get the property's
descriptor via Object.getOwnPropertyDescriptor(object, propertyName) and only
call Object.defineProperty when the descriptor is absent or
writable/configurable are not already false (same guard logic used later in the
else branch). Keep the existing behavior of resolving RawConfig and freezing
arrays but wrap those Object.defineProperty calls with the descriptor check so
makeImmutable is idempotent for top-level array and RawConfig properties.
In `@test/0-util.js`:
- Around line 131-133: Update the test description to match what is being
asserted: change the it(...) description string that currently reads "The
makeHidden() method is available" to refer to makeImmutable (e.g., "The
makeImmutable() method is available") so the sentence corresponds to the
asserted symbol util.makeImmutable in the test case.
🧹 Nitpick comments (7)
lib/util.js (3)
159-161: MissingwritableandconfigurableindefinePropertyfor arrays.When only
valueis specified inObject.definePropertyand the property already exists as a data descriptor,writableandconfigurabledefault tofalseonly for new property definitions. For existing properties being redefined, they default to the existing values. Explicitly settingwritable: false, configurable: falsehere (matching the other branches) would make the intent clear and avoid subtle inconsistencies.Proposed fix
Object.defineProperty(object, propertyName, { - value: Object.freeze(value) + value: Object.freeze(value), + writable: false, + configurable: false });
167-190: Proxygettrap useshasOwnProperty— considerObject.hasOwnorObject.prototype.hasOwnProperty.call.Line 171 uses
target.hasOwnProperty(property). If the target object was created withObject.create(null), it won't havehasOwnProperty. Config objects are unlikely to hit this, but defensive coding would useObject.prototype.hasOwnProperty.call(target, property)or the more modernObject.hasOwn(target, property).
913-918:Load.clone()double-merges theenvobject.The constructor
new Load(this.options, newEnv)setscopy.env = newEnv(already cloned). ThenUtil.extendDeep(copy, this)iteratesthis's enumerable properties includingenv, which merges the original's env data into the already-cloned copy. This is redundant (same data) but wasteful. Consider excludingenvfrom the extend, or simplifying:Alternative approach
clone() { const newEnv = this.env.clone(); - const copy = new Load(this.options, newEnv); - - return Util.extendDeep(copy, this); + const copy = new Load(this.options, newEnv); + copy.config = Util.cloneDeep(this.config); + copy.sources = this.sources ? [...this.sources] : undefined; + copy.defaults = this.defaults ? Util.cloneDeep(this.defaults) : undefined; + copy.unmerged = this.unmerged ? Util.cloneDeep(this.unmerged) : undefined; + return copy; }lib/config.js (4)
294-309: DoubleObject.preventExtensionscall.
Util.makeImmutable(object)at line 304 already callsObject.preventExtensions(object)at the end (lib/util.js line 205). Line 305 calls it again. Harmless but redundant.Remove the redundant call
} else { Util.makeImmutable(object); - Object.preventExtensions(object); return object; }
342-429: Significant code duplication betweenmakeImmutablePartialandUtil.makeImmutable.The proxy creation logic, descriptor checks, array handling, and RawConfig handling in
makeImmutablePartial(lines 360-422) are near-identical toUtil.makeImmutable(lib/util.js lines 142-203). The only difference is thatmakeImmutablePartialoperates on a subset of properties rather than all keys.Consider extracting the shared per-property immutability logic into a helper on
Utilthat both paths can call, reducing the maintenance burden of keeping two copies in sync.
382-392: Unnecessary array spread and O(n)includesin proxygettrap.Line 384-386 creates a new array from
Object.getOwnPropertyNames(target)via spread, then usesincludes()for lookup — both are O(n) per property access. In contrast,Util.makeImmutableusestarget.hasOwnProperty(property)which is O(1). For consistency and performance:Align with Util.makeImmutable
get(target, property, receiver) { - const ownProps = [ - ...Object.getOwnPropertyNames(target), - ] - - // Bypass proxy receiver for properties directly on the target (e.g., RegExp.prototype.source) - // or properties that are not functions to prevent errors related to internal object methods. - if (ownProps.includes(property) || (property in target && typeof target[property] !== 'function')) { + if (Object.prototype.hasOwnProperty.call(target, property) || (property in target && typeof target[property] !== 'function')) { return Reflect.get(target, property); }
540-567:attachProtoDeepsilently skips attaching if a property with the same name exists.Line 553:
if (!toObject[fnName])— if a config key happens to be namedget,has, orutil, the prototype method won't be attached and that sub-object's.get()/.has()will silently behave differently. This is pre-existing behavior, but worth noting as a potential footgun for users with these config key names.
69e6f13 to
f98e04d
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@lib/config.js`:
- Around line 537-553: The attachProtoDeep method currently skips attaching
prototype helpers when the target object has truthy properties named 'get',
'has', 'util', or LOAD_SYMBOL, causing user keys to shadow methods; update the
conditional inside the for-loop that iterates over ['get','has','util',
LOAD_SYMBOL] to attach the hidden method when the existing property is not a
function (e.g., use typeof to check for 'function') so that real methods are
always installed via Util.makeHidden; reference attachProtoDeep, RawConfig,
Util.makeHidden and the fnName check to locate and update the logic, and add a
brief comment explaining the typeof-based guard to prevent silent shadowing.
- Around line 339-426: Refactor makeImmutablePartial to reuse Util.makeImmutable
instead of duplicating logic: for each property in makeImmutablePartial, if the
value is a RawConfig call value.resolve(), if it's an array call
Util.makeImmutable on any object/array items then Object.freeze the array,
otherwise if Util.isObject(value) call Util.makeImmutable(value) (removing the
inline Proxy/descriptor recursion), then apply the same descriptor guard (check
Object.getOwnPropertyDescriptor and only call Object.defineProperty with
writable:false, configurable:false when needed) and retain the final
Object.preventExtensions(object); replace the duplicated
proxy/descriptor/recursion blocks with calls to Util.makeImmutable to ensure
single-source immutability behavior.
- Around line 339-426: The makeImmutablePartial function unconditionally calls
Object.defineProperty in the RawConfig and Array.isArray branches which can
throw on repeated calls; update those branches (the RawConfig branch that
handles value instanceof RawConfig and the Array.isArray(value) branch) to first
retrieve the current descriptor via Object.getOwnPropertyDescriptor(object,
propertyName) and only call Object.defineProperty if no descriptor exists or its
writable/configurable are not already false (same guard used later in
makeImmutablePartial), ensuring idempotent behavior.
In `@test/0-util.js`:
- Around line 159-161: The Array branch in Util.makeImmutable currently defines
array properties with only a frozen value, leaving the descriptor
writable/configurable; update the Object.defineProperty call inside
Util.makeImmutable's array-handling branch to include writable: false and
configurable: false (so the property descriptor matches the RawConfig and object
branches) for the propertyName/value pair to enforce immutability.
🧹 Nitpick comments (3)
lib/util.js (3)
167-173: Avoid callinghasOwnPropertydirectly on the target object.
target.hasOwnProperty(property)can throw or return incorrect results if the target has anullprototype or overrideshasOwnProperty. UseObject.prototype.hasOwnProperty.call(target, property)orObject.hasOwn(target, property)(Node ≥ 16.9).Proposed fix
- if (target.hasOwnProperty(property) || (property in target && typeof target[property] !== 'function')) { + if (Object.hasOwn(target, property) || (property in target && typeof target[property] !== 'function')) {
753-760:errorOncelooks correct; minor style nit.
new Error;on line 756 works but parentheses (new Error()) are conventional. Otherwise, the one-time logging approach is sound for its intended purpose.
909-918:Load.clone()— clonedenvgets re-merged byextendDeep.
extendDeep(copy, this)will recursively mergethis.envintocopy.env, even thoughcopywas already constructed with a freshnewEnv. The deep-merge effectively duplicates the work ofthis.env.clone(). This isn't buggy today becauseextendDeepmerges object properties rather than replacing the reference, but it's fragile — ifEnvever gains non-plain-object state (methods storing closures, symbols, etc.), the merge could introduce subtle issues.Consider explicitly skipping
envin the extend, or simplifying to only extend the fields that need copying (config,sources,defaults,unmerged).
53cab6a to
dcb3a4f
Compare
markstos
left a comment
There was a problem hiding this comment.
Thanks for this as well.
… compatibility functions. Deprecating use of property enumeration.
dcb3a4f to
34c11e6
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/config.js (1)
332-341:⚠️ Potential issue | 🟡 Minor
makeImmutablePartialwith a string property doesn't callObject.preventExtensions.When
propertyis a string (line 335), the method returns early at line 336-340 viareturn Object.defineProperty(...), skippingObject.preventExtensionsat line 418. This may be intentional for single-property immutability, but it's inconsistent with the array-of-properties path which always callspreventExtensions.
🤖 Fix all issues with AI agents
In `@lib/util.js`:
- Around line 153-163: The array branch in makeImmutable leaves the property
descriptor writable/configurable true by only passing value to
Object.defineProperty, so update the Object.defineProperty call in makeImmutable
(the block handling Array.isArray(value)) to set writable: false and
configurable: false (matching the RawConfig and scalar/object branches) while
still freezing the array contents with Object.freeze(value); this ensures the
property cannot be reassigned after makeImmutable is called.
🧹 Nitpick comments (1)
lib/util.js (1)
207-207: Nit: missing semicolon.Consistent with the rest of the codebase, consider adding a semicolon after
Object.preventExtensions(object).Proposed fix
- Object.preventExtensions(object) + Object.preventExtensions(object);
This PR builds on top of #864
The second commit relocates all but the selective immutability code and puts it into Util.
A subsequent PR will close a substantial bug in how makeImmutable works. IMO, #865 is a bug, so it will not receive a warning or a year grace period.
Summary by CodeRabbit
New Features
Deprecations
Tests