Skip to content

Nested namespace spread under a string-literal key reports all exports as unused (follow-up to #1669) #2002

Description

@atheer-alhamami

Prerequisites

Reproduction url

https://gist.github.com/atheer-alhamami/e7793e475da8f531b2305724eb9646f7

Reproduction access

  • I've made sure the reproduction is publicly accessible

Description of the issue

Follow-up to #1669, which fixed namespace imports used as object property values. That fix handles a nested spread when the enclosing property key is an identifier, but not when it is a string literal. The two are semantically identical references, so per the Namespace Imports guide (const spread = { ...NS } → all exports used) both should count as used.

Key Value
Knip version 6.34.0 (also reproduced on 6.32.2)
Node.js v24
OS macOS

Reproduction

index.ts — the two objects differ only in the quoting of one key:

import * as identNs from './identKey';
import * as literalNs from './literalKey';

// Identifier key: the nested spread is found, identAlpha counts as used.
export const withIdentifierKey = { queue: { handlers: { ...identNs } } };

// String-literal key: the nested spread is never visited, so literalAlpha
// is reported as an unused export even though the reference is identical.
export const withLiteralKey = { 'my-queue': { handlers: { ...literalNs } } };

npx knip reports:

Unused exports (1)
literalAlpha  literalNs  function  literalKey.ts:1:17

identAlpha is correctly treated as used. literalAlpha is not.

--trace-export literalAlpha shows the reference being resolved and then rejected:

literalKey.ts:literalAlpha
└── index.ts:importNS[literalNs.literalAlpha] ✗

Likely cause

packages/knip/src/typescript/visitors/exports.ts, in the findSpreads helper (~line 137). The recursive branch only descends into properties whose key is an Identifier:

} else if (
  prop.type === 'Property' &&
  prop.value?.type === 'ObjectExpression' &&
  prop.key?.type === 'Identifier'      // string-literal keys are never recursed
) {
  findSpreads(prop.value, [...path, prop.key.name]);
}

With a quoted key the walk stops one level above the SpreadElement, so the spread is never registered. Accepting a Literal/StringLiteral key here (using its value for the path segment) would appear to cover it, though I have not tested a patch.

Why it matters in practice

This shows up wherever a registry object is keyed by strings that are not valid identifiers. In our case it is a Temporal worker registry keyed by task-queue names:

export const workerConfigs: Record<string, WorkerQueueConfig> = {
  'outbound-calls': {
    activities: { ...callActivities, ...telemetryRecordActivities },
    // ...
  },
  'qa-generation': { /* ... */ },
};

Every hyphenated queue name is a string literal, so every activity spread beneath one reports as unused. Renaming the keys is not an option — they are the queue identifiers the workers register under — so the only workaround we found is declaring the module as an entry, which then suppresses genuinely dead exports in it too.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions