Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(utils): add merge and isPlainObject #946

Merged
merged 7 commits into from
Nov 23, 2024
Merged

feat(utils): add merge and isPlainObject #946

merged 7 commits into from
Nov 23, 2024

Conversation

cheton
Copy link
Member

@cheton cheton commented Nov 12, 2024

PR Type

enhancement, tests


Description

  • Introduced merge function in shared.js to merge objects with deep clone capability.
  • Added isPlainObject function in assertion.js to check if a value is a plain object.
  • Enhanced test coverage by adding tests for merge and isPlainObject.
  • Refined existing tests for isEmptyArray, isEmptyObject, ariaAttr, and dataAttr.
  • Updated documentation to reflect new utility functions.

Changes walkthrough 📝

Relevant files
Enhancement
assertion.js
Add `isPlainObject` function to assertion utilities           

packages/utils/src/assertion.js

  • Added isPlainObject function to check if a value is a plain object.
  • +10/-0   
    shared.js
    Add `merge` function with deep clone capability                   

    packages/utils/src/shared.js

  • Added merge function to merge objects.
  • Implemented deep clone functionality for objects.
  • +37/-0   
    Tests
    assertion.test.js
    Add tests for `isPlainObject` and refine existing tests   

    packages/utils/src/tests/assertion.test.js

  • Added tests for isPlainObject.
  • Removed redundant test cases for isEmptyArray and isEmptyObject.
  • +50/-15 
    shared.test.js
    Add tests for `merge` function and refine attribute tests

    packages/utils/src/tests/shared.test.js

  • Added tests for merge function.
  • Separated tests for ariaAttr and dataAttr.
  • +126/-10
    index.test.js
    Update export tests to include new utilities                         

    packages/utils/tests/index.test.js

    • Updated expected exports to include isPlainObject and merge.
    +2/-0     
    Documentation
    tonic-ui-945-utils.md
    Document new utility functions in changeset                           

    .changeset/tonic-ui-945-utils.md

    • Documented the addition of merge and isPlainObject functions.
    +5/-0     

    💡 PR-Agent usage: Comment /help "your question" on any pull request to receive relevant information

    Copy link

    codesandbox bot commented Nov 12, 2024

    Review or Edit in CodeSandbox

    Open the branch in Web EditorVS CodeInsiders

    Open Preview

    Copy link

    changeset-bot bot commented Nov 12, 2024

    🦋 Changeset detected

    Latest commit: 90c3790

    The changes in this PR will be included in the next version bump.

    This PR includes changesets to release 1 package
    Name Type
    @tonic-ui/utils Minor

    Not sure what this means? Click here to learn what changesets are.

    Click here if you're a maintainer who wants to add another changeset to this PR

    Copy link
    Contributor

    codiumai-pr-agent-free bot commented Nov 12, 2024

    PR Reviewer Guide 🔍

    (Review updated until commit 0642b3d)

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 PR contains tests
    🔒 Security concerns

    Prototype Pollution:
    While the code includes tests for prototype pollution prevention, the merge function implementation could still be vulnerable to prototype pollution attacks through nested object properties that bypass the isPlainObject check. Additional validation may be needed for nested object properties.

    ⚡ Recommended focus areas for review

    Possible Bug
    The merge function may not properly handle circular references in objects that are not caught by WeakMap tracking

    Performance Issue
    The isPlainObject implementation checks multiple Symbol properties which could be optimized by checking prototype chain first

    Code Smell
    The _deepClone helper function is not exported but contains complex logic that should be tested independently

    Copy link

    codesandbox-ci bot commented Nov 12, 2024

    This pull request is automatically built and testable in CodeSandbox.

    To see build info of the built libraries, click here or the icon next to each commit SHA.

    Copy link

    codecov bot commented Nov 12, 2024

    Codecov Report

    Attention: Patch coverage is 91.89189% with 3 lines in your changes missing coverage. Please review.

    Project coverage is 78.41%. Comparing base (0b8452b) to head (90c3790).
    Report is 1 commits behind head on v2.

    Files with missing lines Patch % Lines
    packages/utils/src/shared.js 90.62% 3 Missing ⚠️
    Additional details and impacted files
    @@            Coverage Diff             @@
    ##               v2     #946      +/-   ##
    ==========================================
    + Coverage   78.35%   78.41%   +0.06%     
    ==========================================
      Files         405      405              
      Lines        6640     6677      +37     
    ==========================================
    + Hits         5203     5236      +33     
    - Misses       1437     1441       +4     

    ☔ View full report in Codecov by Sentry.
    📢 Have feedback on the report? Share it here.


    🚨 Try these New Features:

    @trendmicro-frontend-bot
    Copy link
    Contributor

    trendmicro-frontend-bot commented Nov 12, 2024

    Tonic UI Demo

    On 2024-11-23 07:02:58 +0000, PR #946 (90c3790) was successfully deployed. You can view it at the following link:
    https://trendmicro-frontend.github.io/tonic-ui-demo/react/pr-946/

    @cheton cheton force-pushed the tonic-ui-945-utils branch from 60e71c8 to cd713f4 Compare November 12, 2024 16:01
    @cheton cheton changed the title feat(utils): add deepmerge and isPlainObject feat(utils): add merge and isPlainObject Nov 18, 2024
    @cheton
    Copy link
    Member Author

    cheton commented Nov 18, 2024

    /improve

    @cheton
    Copy link
    Member Author

    cheton commented Nov 18, 2024

    /review

    Copy link
    Contributor

    Persistent review updated to latest commit 65e7a61

    @cheton
    Copy link
    Member Author

    cheton commented Nov 18, 2024

    /describe

    Copy link
    Contributor

    PR Description updated to latest commit (65e7a61)

    Copy link
    Contributor

    codiumai-pr-agent-free bot commented Nov 18, 2024

    PR Code Suggestions ✨

    Latest suggestions up to 2c81177

    CategorySuggestion                                                                                                                                    Score
    Possible issue
    ✅ Add protection against circular references in deep cloning to prevent stack overflow errors
    Suggestion Impact:The commit implemented a check for circular references in the _deepClone function using a WeakMap to prevent infinite recursion, as suggested.

    code diff:

    -const _deepClone = (source) => {
    +const _deepClone = (source, seen = new WeakMap()) => {
    +  // Use a `WeakMap` to track objects and detect circular references.
    +  // If the object has been cloned before, return the cached cloned version.
    +  if (seen.has(source)) {
    +    return seen.get(source);
    +  }
    +
       if (Array.isArray(source)) {
    -    return source.map((item) => _deepClone(item));
    +    const clonedArray = [];
    +    seen.set(source, clonedArray);
    +    for (let i = 0; i < source.length; ++i) {
    +      clonedArray[i] = _deepClone(source[i], seen);
    +    }
    +    return clonedArray;
       }
     
       if (isPlainObject(source)) {
    -    const output = {};
    -    Object.keys(source).forEach((key) => {
    -      output[key] = _deepClone(source[key]);
    -    });
    -    return output;
    +    const clonedObject = {};
    +    seen.set(source, clonedObject);
    +    for (const [key, value] of Object.entries(source)) {
    +      clonedObject[key] = _deepClone(value, seen);
    +    }
    +    return clonedObject;
       }

    Consider adding a check for circular references in the _deepClone function to
    prevent infinite recursion when cloning objects with circular dependencies.

    packages/utils/src/shared.js [18-33]

    -const _deepClone = (source) => {
    -  if (Array.isArray(source)) {
    -    return source.map((item) => _deepClone(item));
    -  }
    -
    -  if (isPlainObject(source)) {
    -    const output = {};
    -    Object.keys(source).forEach((key) => {
    -      output[key] = _deepClone(source[key]);
    -    });
    +const _deepClone = (source, seen = new WeakMap()) => {
    +  if (typeof source === 'object' && source !== null) {
    +    if (seen.has(source)) {
    +      return seen.get(source);
    +    }
    +    const output = Array.isArray(source) ? [] : {};
    +    seen.set(source, output);
    +    
    +    if (Array.isArray(source)) {
    +      output.push(...source.map(item => _deepClone(item, seen)));
    +    } else if (isPlainObject(source)) {
    +      Object.keys(source).forEach((key) => {
    +        output[key] = _deepClone(source[key], seen);
    +      });
    +    }
         return output;
       }
    -
       return source;
     };
    • Apply this suggestion
    Suggestion importance[1-10]: 9

    Why: The suggestion addresses a critical security issue by preventing infinite recursion in deep cloning operations when circular references are present, which could cause stack overflow crashes.

    9
    Security
    Add depth limit to prevent stack overflow from extremely deep recursive merges

    Add a size limit check in the merge function to prevent potential stack overflow
    from extremely deep objects.

    packages/utils/src/shared.js [60-65]

    -export const merge = (target, source, options = { clone: true }) => {
    +export const merge = (target, source, options = { clone: true }, depth = 0) => {
    +  if (depth > 1000) {
    +    throw new Error('Maximum merge depth exceeded');
    +  }
       // Merge arrays
       if (Array.isArray(target) && Array.isArray(source)) {
         const output = options.clone ? [...target] : target;
         source.forEach((item, index) => {
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    Why: This suggestion adds an important safety check to prevent potential DoS attacks through maliciously crafted deep objects that could cause stack overflow.

    8
    Enhancement
    ✅ Improve code readability and performance by using Object.entries() for object iteration
    Suggestion Impact:The suggestion to use Object.entries() instead of Object.keys().forEach() was implemented in the commit, improving code readability and performance.

    code diff:

    +    for (const [key, value] of Object.entries(source)) {
    +      if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(output, key) && isPlainObject(output[key])) {
    +        output[key] = merge(output[key], value, options);
           } else {
    -        output[key] = options.clone ? _deepClone(source[key]) : source[key];
    +        output[key] = options.clone ? _deepClone(value) : value;
           }
    -    });
    +    }

    Consider using Object.entries() instead of Object.keys().forEach() for better
    readability and to avoid accessing object properties inside the loop.

    packages/utils/src/shared.js [77-83]

    -Object.keys(source).forEach((key) => {
    -  if (isPlainObject(source[key]) && isPlainObject(output[key]) && Object.prototype.hasOwnProperty.call(output, key)) {
    -    output[key] = merge(output[key], source[key], options);
    +for (const [key, value] of Object.entries(source)) {
    +  if (isPlainObject(value) && isPlainObject(output[key]) && Object.prototype.hasOwnProperty.call(output, key)) {
    +    output[key] = merge(output[key], value, options);
       } else {
    -    output[key] = options.clone ? _deepClone(source[key]) : source[key];
    +    output[key] = options.clone ? _deepClone(value) : value;
       }
    -});
    +}
    • Apply this suggestion
    Suggestion importance[1-10]: 4

    Why: While the suggestion improves code readability and slightly optimizes property access, the impact is relatively minor as the current implementation is already functional and clear.

    4

    Previous suggestions

    ✅ Suggestions up to commit 65e7a61
    CategorySuggestion                                                                                                                                    Score
    Enhancement
    ✅ Add special handling for array types in object merging function
    Suggestion Impact:The commit added logic to handle merging of arrays in the merge function, which aligns with the suggestion to handle arrays properly.

    code diff:

    +  // Merge arrays
    +  if (Array.isArray(target) && Array.isArray(source)) {
    +    const output = options.clone ? [...target] : target;
    +    source.forEach((item, index) => {
    +      if (isPlainObject(item) && isPlainObject(output[index])) {
    +        output[index] = merge(output[index], item, options);
           } else {
    -        output[key] = source[key];
    +        output[index] = options.clone ? _deepClone(item) : item;
           }
         });
    +    return output;

    The merge function should handle arrays properly since they are also objects but
    require different merging logic.

    packages/utils/src/shared.js [60-61]

    +if (Array.isArray(source)) {
    +  return options.clone ? [...source] : source;
    +}
     if (isPlainObject(target) && isPlainObject(source)) {
       Object.keys(source).forEach((key) => {
    Suggestion importance[1-10]: 8

    Why: The suggestion addresses an important edge case in the merge function by properly handling arrays, which could prevent unexpected behavior when merging array-containing objects.

    8
    Possible bug
    Add defensive programming by validating input parameters before processing

    Consider adding a null/undefined check for the source parameter to prevent potential
    runtime errors when merging objects.

    packages/utils/src/shared.js [57-58]

     export const merge = (target, source, options = { clone: true }) => {
    +  if (!source) return target;
       const output = options.clone ? { ...target } : target;
    Suggestion importance[1-10]: 7

    Why: Adding a null/undefined check for the source parameter is a good defensive programming practice that prevents potential runtime errors and improves code robustness.

    7
    Best practice
    Improve type checking accuracy for plain objects

    Add a check for primitive values wrapped in Object to ensure more accurate plain
    object detection.

    packages/utils/src/assertion.js [34-37]

     export const isPlainObject = (value) => {
    -  if (typeof value !== 'object' || value === null) {
    +  if (typeof value !== 'object' || value === null || Object(value) !== value) {
         return false;
       }
    Suggestion importance[1-10]: 6

    Why: The additional check for primitive values wrapped in Object would make the isPlainObject function more robust, though the current implementation already handles most common cases correctly.

    6
    Suggestions up to commit 5136f4e
    CategorySuggestion                                                                                                                                    Score
    Possible bug
    Add input validation to prevent errors from invalid arguments

    Add input validation to ensure target and source are objects before proceeding with
    the merge. This prevents potential errors when invalid inputs are provided.

    packages/utils/src/shared.js [57-58]

     export const deepmerge = (target, source, options = { clone: true }) => {
    +  if (!target || typeof target !== 'object') return source;
    +  if (!source || typeof source !== 'object') return target;
       const output = options.clone ? { ...target } : target;
    Suggestion importance[1-10]: 8

    Why: The suggestion adds crucial input validation to prevent potential runtime errors and unexpected behavior when invalid arguments are passed to the deepmerge function.

    8
    Best practice
    Use modern Object.hasOwn() method for property ownership checks

    Consider using Object.hasOwn() instead of Object.prototype.hasOwnProperty.call() for
    a more modern and safer property ownership check.

    packages/utils/src/shared.js [64]

    -Object.prototype.hasOwnProperty.call(target, key)
    +Object.hasOwn(target, key)
    Suggestion importance[1-10]: 5

    Why: Using Object.hasOwn() is a cleaner and more modern approach than Object.prototype.hasOwnProperty.call(), though both are functionally correct. The improvement is mainly for code maintainability.

    5
    Performance
    Cache repeated function call results to improve performance

    Cache the result of Object.getPrototypeOf(value) to avoid multiple calls to the same
    function with the same argument.

    packages/utils/src/assertion.js [39-40]

     const prototype = Object.getPrototypeOf(value);
    -return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value);
    +const prototypeOfPrototype = prototype && Object.getPrototypeOf(prototype);
    +return (prototype === null || prototype === Object.prototype || prototypeOfPrototype === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value);
    Suggestion importance[1-10]: 4

    Why: The suggestion optimizes performance by avoiding a repeated Object.getPrototypeOf() call, though the performance impact would be minimal in most use cases.

    4

    Copy link
    Contributor

    Persistent review updated to latest commit 2c81177

    @cheton
    Copy link
    Member Author

    cheton commented Nov 18, 2024

    /review

    @cheton
    Copy link
    Member Author

    cheton commented Nov 18, 2024

    /improve

    Copy link
    Contributor

    codiumai-pr-agent-free bot commented Nov 18, 2024

    PR Code Suggestions ✨

    No code suggestions found for the PR.

    Copy link
    Contributor

    Persistent review updated to latest commit 0642b3d

    @cheton
    Copy link
    Member Author

    cheton commented Nov 23, 2024

    /ask enhance test coverae for the merge function with arrays

    Copy link
    Contributor

    Ask

    enhance test coverae for the merge function with arrays

    Answer:

    To enhance test coverage for the merge function specifically with arrays, you can add test cases that cover various scenarios involving array merging. Here are some suggestions for additional test cases:

    1. Merging Arrays with Primitive Values:

      • Test merging two arrays containing primitive values (e.g., numbers, strings) to ensure they are combined correctly.
      it('should merge arrays with primitive values', () => {
        const target = [1, 2, 3];
        const source = [4, 5, 6];
        const result = merge(target, source);
        expect(result).toEqual([4, 5, 6]);
      });
    2. Merging Arrays with Objects:

      • Test merging arrays where elements are objects to ensure deep merging of objects within arrays.
      it('should merge arrays with objects', () => {
        const target = [{ a: 1 }, { b: 2 }];
        const source = [{ a: 3 }, { c: 4 }];
        const result = merge(target, source);
        expect(result).toEqual([{ a: 3 }, { c: 4 }]);
      });
    3. Handling Arrays of Different Lengths:

      • Test merging arrays of different lengths to verify that the longer array's elements are preserved.
      it('should handle arrays of different lengths', () => {
        const target = [1, 2];
        const source = [3, 4, 5];
        const result = merge(target, source);
        expect(result).toEqual([3, 4, 5]);
      });
    4. Merging Nested Arrays:

      • Test merging arrays that contain nested arrays to ensure proper handling of nested structures.
      it('should merge nested arrays', () => {
        const target = [[1, 2], [3, 4]];
        const source = [[5, 6], [7, 8]];
        const result = merge(target, source);
        expect(result).toEqual([[5, 6], [7, 8]]);
      });
    5. Ensuring Cloning Behavior:

      • Test the cloning behavior when merging arrays to ensure that changes to the result do not affect the original arrays.
      it('should clone arrays when merging', () => {
        const target = [1, 2, 3];
        const source = [4, 5, 6];
        const result = merge(target, source, { clone: true });
        result[0] = 10;
        expect(target).toEqual([1, 2, 3]);
        expect(source).toEqual([4, 5, 6]);
      });

    By adding these test cases, you can ensure that the merge function behaves correctly when dealing with arrays in various scenarios.

    @cheton cheton force-pushed the tonic-ui-945-utils branch from 83700a4 to 90c3790 Compare November 23, 2024 06:55
    @cheton cheton merged commit 22ccadf into v2 Nov 23, 2024
    7 checks passed
    @cheton cheton deleted the tonic-ui-945-utils branch November 23, 2024 07:40
    cheton added a commit that referenced this pull request Nov 23, 2024
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    2 participants