Skip to content

linter: import/no-cycle false negative depends on filename/traversal order with type-only import path #19245

@AvenJose

Description

@AvenJose

Summary

oxlint import/no-cycle can miss a real cycle depending only on sibling filename/traversal order.

I can reproduce a deterministic false negative where:

  • Case A reports 0 diagnostics
  • Case B (same graph shape, only renaming aaaInternal.ts -> zzzInternal.ts) reports the expected 3 cycle diagnostics

This looks like traversal-order sensitivity, likely interacting with a type-only import path.

Environment

  • oxlint: 1.42.0
  • Node: v22.20.0
  • OS: Darwin 24.6.0 arm64

Repro

Create this file tree:

src/manager/balanceSweepDetailsManager.ts

import { installmentLoanManager } from './installmentLoanManager'
import { aaaInternal } from './aaaInternal'

export const balanceSweepDetailsManager = {
  call(): string {
    return installmentLoanManager.call() + aaaInternal.call()
  },
}

src/manager/installmentLoanManager.ts

import { getAvenAccountData } from './dataReport/avenAccountDataReportManager'
export type installType = { x: number }
export const installmentLoanManager = { call: () => getAvenAccountData() }

src/manager/dataReport/avenAccountDataReportManager.ts

import { balanceSweepDetailsManager } from '../balanceSweepDetailsManager'
export const getAvenAccountData = (): string => String(Boolean(balanceSweepDetailsManager))

src/manager/simpleInterestLoanManager.ts

import type { installType } from './installmentLoanManager'
export const simpleInterestLoanManager = { call: (): string => String(Boolean(null as installType | null)) }

src/manager/aaaInternal.ts

import { simpleInterestLoanManager } from './simpleInterestLoanManager'
export const aaaInternal = { call: () => simpleInterestLoanManager.call() }

Run:

oxlint --import-plugin -A all -D no-cycle -f json src

Actual (Case A)

diagnostics.length = 0

Expected

Should report the cycle:
balanceSweepDetailsManager -> installmentLoanManager -> dataReport/avenAccountDataReportManager -> balanceSweepDetailsManager

Filename-only rename flips behavior

Rename only:

  • src/manager/aaaInternal.ts -> src/manager/zzzInternal.ts
  • update the import in balanceSweepDetailsManager.ts

Run the same command again.

Actual (Case B)

diagnostics.length = 3, with files:

  • src/manager/balanceSweepDetailsManager.ts
  • src/manager/installmentLoanManager.ts
  • src/manager/dataReport/avenAccountDataReportManager.ts

Notes

I also reproduced this pattern in a larger codebase. The minimal repro above is deterministic on my machine.

Metadata

Metadata

Assignees

Labels

A-linterArea - LinterC-bugCategory - Bug

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions