Skip to content

Fix critical bugs and add tests for all remaining untested JS sibling files - #284

Merged
loiane merged 4 commits into
mainfrom
loiane/fix-hash-table-bugs-improve-coverage
Sep 25, 2026
Merged

loiane merged 4 commits into
mainfrom
loiane/fix-hash-table-bugs-improve-coverage

Conversation

@loiane

@loiane loiane commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-up to #283. That PR fixed bugs in the two hash-table .js files. While investigating, I found that nearly every other hand-written plain-JS "sibling" implementation (parallel to the tested TypeScript versions) also had zero test coverage — never require()'d by any test. This PR adds dedicated tests for all of them and fixes every real bug found along the way.

Result: 677 tests passing, statement coverage 94.3% → 98.65%.

Bugs fixed

src/10-tree/

  • fenwick-tree.js / segment-tree.js: missing module.exports — these classes could never be require()'d at all.
  • binary-search-tree.js, avl-tree.js, red-black-tree.js: internal require()s lacked explicit .js extensions, resolving to the .ts siblings instead and throwing at construction time.
  • avl-tree.js & red-black-tree.js: both subclasses declared their own private #root, shadowing the base class's #root used by inherited search/min/max/traversal methods. These were silently broken — search always returned false, min/max always null, traversals were no-ops — for both classes, the whole time.
  • red-black-tree.js: rotation helpers never returned the new subtree root, corrupting the tree on removal-triggered rebalancing. Also fixed a stale parent pointer left after promoting a single child during removal.

src/07-set/set.js

  • has() / getSizeWithoutSizeProperty() called this.#items.hasOwnProperty() directly — throws if a value like "hasOwnProperty" was ever stored, since it shadows the prototype method.

src/08-dictionary-hash/hash-table.js

  • put() used && instead of || for its null-check (allowed a null key through). remove() used a truthy check that could never remove falsy values (0, '', false). toString() crashed on an empty table and mis-formatted objects in the first bucket.

src/06-linked-list/circular-linked-list_.js

  • prepend() mutated a stray public head property instead of the private #head, silently desyncing the list.
  • removeAt() called a nonexistent #removeFromMiddle method — a hard crash for any middle-position removal.
  • remove()/indexOf() contained leftover TypeScript type annotations, which are invalid syntax in a plain .js file and broke module parsing entirely.

queue.js, stack.js, doubly-linked-list_.js

  • toString() called item.toString() on primitives, throwing on undefined items. Switched to String(item) to match the .ts siblings.

src/13-graph/*.js, src/12-trie/trie.js: no .js-specific bugs found — added full test coverage (100% stmts/branch) as a safety net.

Known pre-existing issues found but intentionally left unfixed

(identical in both .js and .ts, so out of scope for this .js-only pass)

  • kruskal.js/.ts: find() uses a falsy parent-pointer check that mistreats vertex 0 as "no parent," which can leave a vertex disconnected from the MST result.
  • segment-tree.js/.ts: the "no overlap" query base case always returns 0, which is only a valid identity for sum, not min/max.
  • red-black-tree.js/.ts: rare duplicate-value delete sequences can produce a structurally invalid BST (fuzz-tested 300+ trials with distinct values — no failures).

Tests added

New __test__ files for every previously-untested .js module: set-js, trie-js, avl-tree-js, binary-search-tree-js, red-black-tree-js, fenwick-tree-js, segment-tree-js, graph-js, bfs-js, dfs-js, dijkstra-js, floyd-warshall-js, kruskal-js, prim-js, queue-js, stack-js, doubly-linked-list_, circular-linked-list_, hash-table-js.


Update: the #root-shadowing bug also existed in the TypeScript files

While investigating whether these .js bugs were pre-existing "compiled output" issues, I confirmed .js and .ts are hand-written, independently-maintained parallel implementations (no build step relates them — tsconfig.json has no outDir, and some .js files here have no .ts counterpart at all).

Checking each .js fix against its .ts sibling, most were .js-only (set.ts, hash-table.ts, circular-linked-list.ts, queue.ts/stack.ts/doubly-linked-list.ts were all already correct). However, avl-tree.ts and red-black-tree.ts — the actively-tested TypeScript reference implementations — had the exact same bugs, now fixed in the latest commit:

  • #root shadowing (both classes): identical to the .js bug — search() always returned false, min()/max() always null, root always null, traversals were no-ops, despite insert/remove working correctly.
  • red-black-tree.ts only: #rotateLeft/#rotateRight were typed to return void and had no return statement, so #balance()'s return this.#rotateRight(node) calls (used during removal rebalancing) returned undefined, silently corrupting the tree. Also fixed a stale parent pointer left after promoting a single child during removal.

This was never caught because binary-search-tree.test.ts's AVLTree/RedBlackTree test blocks only asserted expect(() => ...).not.toThrow() — they never called search()/min()/max()/checked root, or verified traversal output. Added correctness assertions to close that gap.

Updated result: 687 tests passing, tsc --noEmit clean.

… files

Extends the previous pass to cover every core data-structure .js
implementation that had zero test coverage (never required by any
test). Adds dedicated test files and fixes real bugs found along the
way:

Trees (src/10-tree):
- fenwick-tree.js / segment-tree.js: missing module.exports meant
  these classes could never be require()'d at all.
- binary-search-tree.js, avl-tree.js, red-black-tree.js: internal
  require()s lacked explicit .js extensions, so they resolved to the
  .ts siblings under this project's module resolution order, throwing
  at construction time.
- avl-tree.js and red-black-tree.js: both subclasses declared their
  own private #root field, shadowing the base class's #root used by
  inherited search/min/max/traversal methods and the root getter.
  These were silently broken (search always false, min/max always
  null, traversals no-ops) for both classes. Now override affected
  methods to use their own #root.
- red-black-tree.js: rotation helpers never returned the new subtree
  root, corrupting the tree on removal-triggered rebalancing. Also
  fixed a stale parent pointer left after promoting a single child
  during node removal.

Graph algorithms (src/13-graph): no .js-specific bugs found; added
full test coverage for graph/bfs/dfs/dijkstra/floyd-warshall/kruskal/
prim.

Hash table, set, trie:
- set.js: has()/getSizeWithoutSizeProperty() called
  this.#items.hasOwnProperty() directly, which throws if a value like
  "hasOwnProperty" was ever stored (shadows the prototype method).
- hash-table.js: put() used && instead of || for its null-check,
  remove() used a truthy check that could never remove falsy values,
  and toString()/#elementToString() crashed on an empty table and
  mis-formatted objects for the first bucket.
- trie.js: no bugs found, added full coverage.

Queue/stack/linked lists:
- circular-linked-list_.js: prepend() mutated a stray public head
  property instead of the private #head, silently desyncing the list;
  removeAt() called a nonexistent #removeFromMiddle method (added it);
  remove()/indexOf() contained leftover TypeScript type annotations
  that are invalid in a plain .js file, breaking module parsing
  entirely.
- queue.js, stack.js, doubly-linked-list_.js: toString() called
  item.toString() on primitives, throwing on undefined items; switched
  to String(item) to match the .ts siblings.

All 677 tests pass; overall statement coverage 94.3% -> 98.65%.

Known, pre-existing issues found but intentionally left unfixed because
they are identical in both the .js and .ts implementations (out of
scope for a .js-only bug-fix pass):
- kruskal.js/.ts: find() uses a falsy parent-pointer check that
  mistreats vertex 0 as "no parent", which can leave a vertex
  disconnected from the MST result.
- segment-tree.js/.ts: the no-overlap query base case always returns
  0, which is only a valid identity for sum, not min/max.
- red-black-tree.js/.ts: rare duplicate-value delete sequences can
  produce a structurally invalid BST; distinct-value sequences were
  fuzz-tested (300+ trials) with no failures.
…e.ts

Keep the TypeScript implementations in sync with the equivalent .js fixes
from the previous commit. AVLTree and RedBlackTree each declare their own
private #root field, shadowing BinarySearchTree's private #root. Since
private class fields are keyed per declaring class, the inherited
search()/min()/max()/root/traversal methods always read the base class's
#root, which stays null after construction. As a result, search() always
returned false, min()/max() always returned null, and root was always
null, even though insert()/remove() worked correctly against the
subclass's own #root.

Fix: override root/search/min/max/inOrderTraverse/preOrderTraverse/
postOrderTraverse in AVLTree and RedBlackTree to operate on their own
#root, matching the pattern already used in avl-tree.js/red-black-tree.js.

red-black-tree.ts also had two additional bugs matching ones already
fixed in red-black-tree.js:
- #rotateLeft/#rotateRight were typed to return void and had no return
  statement, so callers relying on their return value (#balance during
  removal) received undefined and could overwrite an already-correct
  parent.left/right link, corrupting the tree.
- #removeNode's single-child promotion branches didn't update the
  promoted child's parent pointer, leaving it stale for any later
  rotation that relies on .parent to relink nodes.

Also strengthens binary-search-tree.test.ts's AVLTree/RedBlackTree
blocks, which previously only asserted `.not.toThrow()` and never
exercised search()/min()/max()/root/traversal output - the reason this
bug was never caught. Added correctness assertions covering search hits
and misses, min/max, root nullability, in-order traversal output, and
removal (including RedBlackTree's single-child promotion path).

Verified: full suite 687/687 passing, `tsc --noEmit` clean.
@loiane
loiane merged commit d71c5af into main Sep 25, 2026
1 check passed
@loiane
loiane deleted the loiane/fix-hash-table-bugs-improve-coverage branch September 25, 2026 18:02
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.

1 participant