Skip to content

Conversation

@jrobinsonc
Copy link
Owner

@jrobinsonc jrobinsonc commented Aug 23, 2025

Summary by CodeRabbit

  • New Features

    • Expanded emptiness rules across more types (strings trimmed, NaN, Maps/Sets/Arrays/TypedArrays by size/length, plain objects including null-prototype); booleans, non-NaN numbers, BigInt, symbols, and functions are never empty. Unknown/unsupported types now raise TypeError with detected tag.
  • Tests

    • Added comprehensive test suite covering primitives, collections, typed arrays, objects, symbol-keyed props, and edge cases.
  • Documentation

    • Clarified emptiness vs. falsiness and broadened type coverage.
  • Chores

    • Stricter linting and TypeScript configuration, package scripts updated, build outputs aligned to ESM (.mjs), and Node engine requirement relaxed.

@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Walkthrough

Refactors isEmpty with explicit per-type emptiness rules (primitives, boxed, arrays, typed arrays, Maps/Sets, plain objects), uses getTag/isPlainObject, throws TypeError for unsupported tags, adds comprehensive Jest tests, relaxes isPlainObject to accept null-prototype objects, and updates lint, package, tsconfig, and rollup configs.

Changes

Cohort / File(s) Summary
Core: Emptiness semantics
src/isEmpty.ts
Major refactor: explicit per-type emptiness rules (null/undefined, booleans, strings trimmed, numbers → NaN only, BigInt, Maps/Sets by size, arrays/typed arrays by length, plain objects by Object.keys), uses getTag and isPlainObject, and throws TypeError with tag for unsupported types.
Core: Plain object detection
src/isPlainObject.ts
Adjusted plain-object detection to treat objects with null prototype as plain (accepts Object.prototype or null), keeping other checks.
Tests: isEmpty coverage
tests/isEmpty.test.ts
New Jest test suite covering booleans, null/undefined, string trim semantics, numbers/NaN, BigInt, arrays/typed arrays, Map/Set, plain and null-prototype objects, non-enumerable and symbol-keyed properties, functions, WeakMap/WeakSet error cases, and TODOs for other built-ins.
Tooling: Lint config
eslint.config.mjs
Replaced ESLint import alias and expanded TypeScript rules into categorized Error/Quality/Style/Performance/Security sets; removed injected node globals; added overrides for **/*.mjs and **/*.test.ts.
Tooling: Package scripts & engines
package.json
Removed top-level main/module/types fields; changed exports import path to ./dist/index.mjs; check-types now emits, lint scripts run eslint ., lint:fix uses --fix, and engines.node relaxed from 22.x to >=18.
Tooling: TypeScript config
tsconfig.json
Large overhaul toward strict, bundler-oriented config: path alias @/*, moduleResolution: "bundler", moduleDetection: "force", stricter type-safety and hygiene options, non-emitting workflow adjustments, and expanded exclude (including dist).
Tooling: Bundling/declarations
rollup.config.mjs
Output ESM import path changed to dist/index.mjs; TypeScript plugin and dts plugin configuration simplified (removed custom compilerOptions/declarationDir settings).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant isEmpty as isEmpty()
  participant getTag as getTag()
  participant isPlain as isPlainObject()

  Caller->>isEmpty: isEmpty(value)
  alt value is null/undefined
    isEmpty-->>Caller: true
  else
    isEmpty->>getTag: tag = getTag(value)
    alt tag is String
      isEmpty-->>Caller: return trimmedString === ''
    else tag is Number
      isEmpty-->>Caller: return Number.isNaN(value)
    else tag is Boolean|BigInt|Symbol|Function
      isEmpty-->>Caller: false
    else tag is Map|Set
      isEmpty-->>Caller: return value.size === 0
    else tag is Array or TypedArray
      isEmpty-->>Caller: return value.length === 0
    else tag is Object
      isEmpty->>isPlain: isPlainObject(value)?
      alt plain
        isPlain-->>isEmpty: yes
        isEmpty-->>Caller: return Object.keys(value).length === 0
      else non-plain
        isPlain-->>isEmpty: no
        isEmpty-->>Caller: throw TypeError(tag)
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit trims each string with care,
Counts maps and sets and arrays laid bare.
Nulls I nestle, NaN I flag,
Null-protos hugged beneath my hat.
Strange tags I sniff — then thump and stare. 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
src/isPlainObject.ts (1)

14-15: Treating null-prototype objects as plain is good; consider cross-realm robustness and avoid double getPrototypeOf.

Current check fails for plain objects coming from another realm (e.g., iframes) because their Object.prototype is not strictly equal to this realm’s Object.prototype. A robust pattern is to accept objects whose prototype is null OR whose prototype’s prototype is null (i.e., some realm’s Object.prototype). Also, compute the prototype once.

Apply this diff:

 export function isPlainObject(
   value: unknown,
 ): value is Record<string, unknown> {
-  return (
-    typeof value === 'object' &&
-    value !== null &&
-    !Array.isArray(value) &&
-    (Object.getPrototypeOf(value) === Object.prototype ||
-      Object.getPrototypeOf(value) === null)
-  );
+  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
+    return false;
+  }
+  const proto = Object.getPrototypeOf(value as object);
+  // Accept: null-proto objects and objects whose proto is a realm's Object.prototype
+  return proto === null || Object.getPrototypeOf(proto) === null;
 }
src/isEmpty.ts (4)

61-63: Whitespace-only strings: consider explicit length check (optional).

trim() === '' is fine. If you want to emphasize intent and avoid accidental truthiness pitfalls, trim().length === 0 can be a tiny readability win. No need to change if you prefer current style.


71-73: Unify wrapper detection with getTag; avoid realm pitfalls for BigInt (optional).

instanceof BigInt can be inconsistent across realms. Since you already depend on getTag, consider using it for wrapper detection and move const tag = getTag(value); earlier so it’s available here.

Apply this diff:

 export function isEmpty(value: unknown): boolean {
   if (value === undefined || value === null) {
     return true;
   }
 
+  // Compute once to support wrapper objects and cross-realm cases consistently.
+  const tag = getTag(value);
+
   if (typeof value === 'boolean' || value instanceof Boolean) {
     return false;
   }
@@
-  if (typeof value === 'bigint' || value instanceof BigInt) {
+  // Treat all BigInt values (primitive or wrapper) as non-empty data.
+  if (typeof value === 'bigint' || tag === '[object BigInt]') {
     return false;
   }
@@
-  const tag = getTag(value);
+  // tag already computed above

Also applies to: 83-84


75-77: Map/Set: add tag-based fallback for cross-realm collections (optional).

instanceof may fail across realms. Since tag is available, you can tighten this up.

-  if (value instanceof Map || value instanceof Set) {
-    return value.size === 0;
-  }
+  if (tag === '[object Map]' || tag === '[object Set]') {
+    return (value as Map<unknown, unknown> | Set<unknown>).size === 0;
+  }

93-96: Use tag for Symbol wrappers to avoid realm quirks.

Replace instanceof Symbol with a tag check to reliably classify Object(Symbol()).

-  if (typeof value === 'symbol' || value instanceof Symbol) {
+  if (typeof value === 'symbol' || tag === '[object Symbol]') {
     return false;
   }
tests/isEmpty.test.ts (3)

83-104: Remove duplicated typed array entries to speed up the suite without losing coverage.

You list each typed array twice. Deduplicate to keep tests lean.

Apply this diff:

-    it.each([
-      ['Uint8Array', Uint8Array],
-      ['Uint8Array', Uint8Array],
-      ['Int8Array', Int8Array],
-      ['Int8Array', Int8Array],
-      ['Uint16Array', Uint16Array],
-      ['Uint16Array', Uint16Array],
-      ['Int16Array', Int16Array],
-      ['Int16Array', Int16Array],
-      ['Uint32Array', Uint32Array],
-      ['Uint32Array', Uint32Array],
-      ['Int32Array', Int32Array],
-      ['Int32Array', Int32Array],
-      ['Float32Array', Float32Array],
-      ['Float32Array', Float32Array],
-      ['Float64Array', Float64Array],
-      ['Float64Array', Float64Array],
-      ['BigInt64Array', BigInt64Array],
-      ['BigInt64Array', BigInt64Array],
-      ['BigUint64Array', BigUint64Array],
-      ['BigUint64Array', BigUint64Array],
-    ])('should check typed array: %s', (_, TypedObject) => {
+    it.each([
+      ['Uint8Array', Uint8Array],
+      ['Int8Array', Int8Array],
+      ['Uint16Array', Uint16Array],
+      ['Int16Array', Int16Array],
+      ['Uint32Array', Uint32Array],
+      ['Int32Array', Int32Array],
+      ['Float32Array', Float32Array],
+      ['Float64Array', Float64Array],
+      ['BigInt64Array', BigInt64Array],
+      ['BigUint64Array', BigUint64Array],
+    ])('should check typed array: %s', (_, TypedObject) => {
       expect(isEmpty(new TypedObject())).toBe(true);
       expect(isEmpty(new TypedObject(2))).toBe(false);
     });

23-31: Add a String wrapper whitespace case for completeness (optional).

You cover new String(''); add a whitespace-only wrapper to mirror the primitive case.

     it('should return true for empty strings', () => {
       expect(isEmpty('')).toBe(true);
       expect(isEmpty('   ')).toBe(true);
       expect(isEmpty('\n\t')).toBe(true);
       expect(isEmpty('\u00A0\u2007\u202F\u3000')).toBe(true); // Unicode spaces
       expect(isEmpty(new String(''))).toBe(true);
+      expect(isEmpty(new String('  '))).toBe(true);
     });

172-174: Consider converting the TODO into explicit assertions (optional).

If your intent is to throw for non-primitive built-ins (Date, Error, RegExp, Promise, ArrayBuffer, DataView), codify that now for clearer guarantees.

Example additions:

   describe('edge cases', () => {
@@
-    it.todo(
-      'Decide: if non-primitive built-ins should be included: Date, Error, RegExp, Promise...',
-    );
+    it('should throw for non-primitive built-ins not explicitly supported', () => {
+      expect(() => isEmpty(new Date())).toThrow(TypeError);
+      expect(() => isEmpty(/x/)).toThrow(TypeError);
+      expect(() => isEmpty(Promise.resolve())).toThrow(TypeError);
+      expect(() => isEmpty(new ArrayBuffer(0))).toThrow(TypeError);
+      expect(() => isEmpty(new DataView(new ArrayBuffer(8)))).toThrow(TypeError);
+    });

If you’d like, I can push a commit updating tests and the implementation if needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b4e1653 and 0f95045.

📒 Files selected for processing (3)
  • src/isEmpty.ts (1 hunks)
  • src/isPlainObject.ts (1 hunks)
  • tests/isEmpty.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/isEmpty.test.ts (1)
src/isEmpty.ts (1)
  • isEmpty (52-102)
src/isEmpty.ts (1)
src/isPlainObject.ts (1)
  • isPlainObject (7-17)
🔇 Additional comments (8)
src/isEmpty.ts (7)

7-48: Docs are clear and helpful—nice articulation of emptiness vs. falsiness.

The rationale and examples will reduce misuse. Good callouts on symbol-only and non-enumerable-only objects being “empty” by design.


57-60: Boolean handling is correct and explicit.

Covers both primitives and wrappers.


65-69: Number handling is solid.

Using value.valueOf() correctly normalizes wrappers; only NaN is treated as empty.


79-81: Array handling is straightforward and correct.

Early return keeps the typed-array regex from matching plain arrays.


85-87: Typed array detection is concise and correct.

Regex covers all typed arrays; using .length is appropriate.


89-91: Plain object emptiness aligns with documented semantics.

Using Object.keys means symbol-only and non-enumerable-only objects are “empty,” matching the docs and tests.


101-101: Throwing TypeError for unsupported types is fine.

Consistent with tests (e.g., WeakMap/WeakSet). Keep as-is.

tests/isEmpty.test.ts (1)

3-4: Verified path alias configuration: The @/* alias is correctly mapped in both tsconfig.json (paths: "@/*": ["./src/*"]) and jest.config.mjs (moduleNameMapper: '^@/(.*)$': '<rootDir>/src/$1'). Tests importing @/isEmpty will resolve successfully.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
package.json (1)

58-58: Engines vs tsconfig baseline mismatch; align Node target.

You extend @tsconfig/node22 but advertise "node": ">=18". Either raise engines or relax the TS baseline to reduce runtime mismatch risk.

Apply one of:

Option A (prefer): bump engines to Node 22+

-    "node": ">=18",
+    "node": ">=22",

Option B: target Node 18 in TS (paired with rollup transpilation)

-  "extends": "@tsconfig/node22/tsconfig.json",
+  "extends": "@tsconfig/node18/tsconfig.json",
tsconfig.json (1)

37-40: Disable preserveConstEnums (no const enum usage detected)

A search for const enum declarations across your src/**/*.ts files returned no matches, so disabling this option will have no impact on your current code. Inlining enum values is generally safer for downstream consumers that don’t compile with tsc.

• Search performed:

rg -nP '^\s*const\s+enum\s+' -g 'src/**/*.ts' || true
--- tsconfig.json
@@ -37,4 +37,4 @@
     "removeComments": true,
     "verbatimModuleSyntax": true,
-    "preserveConstEnums": true,
+    "preserveConstEnums": false,
     "forceConsistentCasingInFileNames": true
eslint.config.mjs (1)

29-34: Replace core no-undef/no-shadow with TS-aware configuration.

Core rules cause false positives in TS (types-as-values, enum shadowing). Prefer TS-ESLint equivalents and/or scope by file type.

Within this block, remove the core rules:

-      'no-shadow': 'error',
-      'no-undef': 'error',

Add a TS-only override to re-enable the TS-aware versions:

   {
+    files: ['**/*.ts'],
+    rules: {
+      'no-shadow': 'off',
+      'no-undef': 'off',
+      '@typescript-eslint/no-shadow': 'warn'
+    }
+  },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0f95045 and 4c82f4b.

📒 Files selected for processing (3)
  • eslint.config.mjs (1 hunks)
  • package.json (2 hunks)
  • tsconfig.json (1 hunks)
🔇 Additional comments (8)
package.json (2)

20-20: check-types remains a pure type check (good).

With tsconfig’s "noEmit": true, this script won’t emit artifacts. Intent matches the script name.


21-22: Repo-wide linting is fine; dist is already ignored in eslint config.

No action needed.

tsconfig.json (2)

43-44: Excluding dist is correct for editor performance and lint noise.


12-15: Bundler moduleResolution with isolatedModules is safe for tests

I’ve checked that:

  • The project uses ts-jest (v29.4.1) with the “default-esm” preset and useESM: true in jest.config.mjs, so ts-jest is already set up for ESM resolution against your root tsconfig.json’s settings.
  • A grep for any require(...), module.exports, or exports. in src/ returned no CommonJS patterns.

Since TypeScript 5.9 (which ts-jest uses) natively supports "moduleResolution": "Bundler" and there’s no CJS interop in your source, the existing test setup will compile modules correctly without further tweaks. You can consider this concern addressed.

eslint.config.mjs (4)

8-10: Base presets selection looks good.


20-25: Downgrading some TS-ESLint rules to warnings for DX is reasonable.


129-136: mjs override to disable type-checked rules is appropriate.


138-144: No action needed: Jest globals are already imported
All .test.ts files explicitly import Jest globals (e.g. import { describe, expect, it } from '@jest/globals';), so ESLint’s core no-undef rule will not report undefined Jest globals. You can leave the existing ESLint configuration for tests as-is.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rollup.config.mjs (1)

6-6: Fix CI lint error: add a type annotation for the Rollup config

ESLint warns “Expected config to have a type annotation.” Add a JSDoc type.

Apply:

-const config = [
+/** @type {import('rollup').RollupOptions[]} */
+const config = [
♻️ Duplicate comments (1)
package.json (1)

31-50: Jest 30 + ts-jest 29 are incompatible; align majors

ts-jest v29 targets Jest 29. Either downgrade Jest to ^29 or upgrade/replace ts-jest.

Two options:

  • Downgrade Jest:
-    "jest": "^30.0.5",
+    "jest": "^29.7.0",
  • Or upgrade ts-jest (when a Jest 30–compatible release is available) or switch to babel-jest.

Optionally verify and surface the config:

#!/bin/bash
set -euo pipefail
echo "jest:" && jq -r '.devDependencies.jest' package.json
echo "ts-jest:" && jq -r '.devDependencies["ts-jest"]' package.json
fd -a 'jest.config.*' -E node_modules | xargs -I{} sh -c 'echo "--- {}"; cat {}'
🧹 Nitpick comments (3)
rollup.config.mjs (1)

28-29: Consider externalizing deps and Node built-ins

For libs, leaving external: [] can accidentally bundle future deps and built-ins. Recommend deriving externals from package.json and builtinModules.

Apply:

-    external: [],
+    external: external,

Add at the top of the file (outside the selected range):

// Top-level additions to support external inference
import { builtinModules } from 'node:module';
import pkg from './package.json' assert { type: 'json' };

const external = [
  ...builtinModules,
  ...builtinModules.map((m) => `node:${m}`),
  ...Object.keys(pkg.dependencies ?? {}),
  ...Object.keys(pkg.peerDependencies ?? {}),
];
package.json (2)

17-17: Revert to noEmit for check-types (avoid stray build artifacts)

Type-check steps typically shouldn’t emit files; rollup handles builds.

Apply:

-"check-types": "tsc --pretty --project tsconfig.json",
+"check-types": "tsc --pretty --noEmit --project tsconfig.json",

6-12: Add a top-level “types” for wider tooling compatibility

Many tools resolve types from the top-level field; keeping it alongside exports is harmless and improves DX.

Apply:

   "exports": {
     ".": {
       "import": "./dist/index.mjs",
       "require": "./dist/index.cjs",
       "types": "./dist/index.d.ts"
     }
   },
+  "types": "./dist/index.d.ts",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4c82f4b and ec2370c.

📒 Files selected for processing (2)
  • package.json (3 hunks)
  • rollup.config.mjs (2 hunks)
🧰 Additional context used
🪛 GitHub Actions: CI
rollup.config.mjs

[warning] 6-6: ESLint: Expected config to have a type annotation. (typescript-eslint/typedef)

🔇 Additional comments (6)
rollup.config.mjs (2)

11-14: ESM output path switch looks good

Changing ES module output to dist/index.mjs aligns with package.json exports.


33-34: dts plugin simplification is fine

Plain dts() keeps things maintainable and matches the single types output.

package.json (4)

8-11: Exports import path update is correct

dist/index.mjs matches the ESM output in rollup.


18-19: Lint script changes look good

Running eslint on the repo root is appropriate with flat config.


55-57: Engines update is sensible

Node >=18 aligns with modern ESM and toolchain requirements.


31-50: No changes required: tslib placement is correct

tsconfig.json does not enable importHelpers, so tslib isn’t used at runtime and may remain in devDependencies.

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