Skip to content

JS-1383 Fix non-deterministic tsconfig discovery order#6478

Merged
zglicz merged 5 commits intomasterfrom
fix/tsconfig-deterministic-order
Feb 27, 2026
Merged

JS-1383 Fix non-deterministic tsconfig discovery order#6478
zglicz merged 5 commits intomasterfrom
fix/tsconfig-deterministic-order

Conversation

@zglicz
Copy link
Contributor

@zglicz zglicz commented Feb 27, 2026

Summary

opendir() returns directory entries in filesystem-native order, which is not guaranteed to be consistent across OSes, filesystems, or runs (e.g. ext4 with dir_index, APFS). Combined with the LIFO stack traversal in findFiles, the order in which tsconfig.json files are discovered varies.

When multiple tsconfigs cover the same source file, whichever tsconfig is processed first in analyzeWithProgram wins — the file is removed from pendingFiles and skipped by all subsequent tsconfigs. This means different runs can analyze the same file with different TypeScript programs (and different compiler options / lib settings), producing non-deterministic analysis results.

Concrete example: In the TypeScript ruling project, src/services/formatting/rulesMap.ts can be pulled into different tsconfigs depending on filesystem order:

  • Successful run (example): tsconfigs discovered in order TypeScript/src/compiler/tsconfig.json, TypeScript/src/harness/tsconfig.jsonrulesMap.ts is picked up by harness/tsconfig.json which has lib: ["es6"]. S7728 correctly fires (arrays have Symbol.iterator).

  • Unsuccessful run (example): tsconfigs discovered in order TypeScript/scripts/importDefinitelyTypedTests/tsconfig.json, TypeScript/scripts/tslint/tsconfig.json, TypeScript/src/server/tsconfig.jsonrulesMap.ts gets transitively pulled into server/tsconfig.json which has lib: ["es5"]. S7728 is suppressed (no Symbol.iterator on arrays in ES5), causing the ruling snapshot to differ.

Fix

Sort tsconfigs in postProcess() once after all files have been discovered:

  • Lookup tsconfigs (sonar.typescript.tsconfigPaths not set): sorted alphabetically for stable ordering
  • Property tsconfigs (sonar.typescript.tsconfigPaths set): sorted by their position in the user-provided list, preserving the intentional ordering

Test plan

  • TypeScript ruling test passes consistently
  • Existing tsconfig store tests still pass

🤖 Generated with Claude Code

opendir() returns entries in filesystem-native order (not guaranteed
to be stable across OSes, filesystems, or runs). Files discovered
via a LIFO stack compound the non-determinism further. When multiple
tsconfigs cover the same file, whichever is processed first wins,
causing different type information (and thus different analysis results)
across runs.

Fix: sort foundLookupTsConfigs and foundPropertyTsConfigs in postProcess()
once all tsconfigs have been discovered, ensuring a stable alphabetical
order before analyzeWithProgram iterates over them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zglicz zglicz requested a review from a team February 27, 2026 08:34
@hashicorp-vault-sonar-prod hashicorp-vault-sonar-prod bot changed the title Fix non-deterministic tsconfig discovery order JS-1383 Fix non-deterministic tsconfig discovery order Feb 27, 2026
@hashicorp-vault-sonar-prod
Copy link

hashicorp-vault-sonar-prod bot commented Feb 27, 2026

JS-1383

Copy link
Contributor

@vdiez vdiez left a comment

Choose a reason for hiding this comment

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

I guess you where right when you pointed out we should be more deterministic when selecting tsconfigs

🤖 Generated with GitHub Actions
@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

Ruling Report

Code no longer flagged (1101 issues)

S4328

vuetify/packages/docs/src/composables/ad.ts:2

     1 | // Composables
>    2 | import { useAdsStore } from '@/store/ads'
     3 | import { useI18n } from 'vue-i18n'
     4 | 

vuetify/packages/docs/src/composables/ad.ts:8

     6 | import { computed } from 'vue'
     7 | import { kebabCase } from 'lodash-es'
>    8 | import { leadingSlash, trailingSlash } from '@/util/routes'
     9 | 
    10 | export const createAdProps = () => ({

vuetify/packages/docs/src/main.ts:12

    10 | 
    11 | // Plugins
>   12 | import { pinia, usePinia } from '@/plugins/pinia'
    13 | import { useGlobalComponents } from '@/plugins/global-components'
    14 | import { useGtag } from '@/plugins/gtag'

vuetify/packages/docs/src/main.ts:13

    11 | // Plugins
    12 | import { pinia, usePinia } from '@/plugins/pinia'
>   13 | import { useGlobalComponents } from '@/plugins/global-components'
    14 | import { useGtag } from '@/plugins/gtag'
    15 | import { useI18n } from '@/plugins/i18n'

vuetify/packages/docs/src/main.ts:14

    12 | import { pinia, usePinia } from '@/plugins/pinia'
    13 | import { useGlobalComponents } from '@/plugins/global-components'
>   14 | import { useGtag } from '@/plugins/gtag'
    15 | import { useI18n } from '@/plugins/i18n'
    16 | import { useLocaleStore } from '@/store/locale'

vuetify/packages/docs/src/main.ts:15

    13 | import { useGlobalComponents } from '@/plugins/global-components'
    14 | import { useGtag } from '@/plugins/gtag'
>   15 | import { useI18n } from '@/plugins/i18n'
    16 | import { useLocaleStore } from '@/store/locale'
    17 | import { usePwa } from '@/plugins/pwa'

vuetify/packages/docs/src/main.ts:16

    14 | import { useGtag } from '@/plugins/gtag'
    15 | import { useI18n } from '@/plugins/i18n'
>   16 | import { useLocaleStore } from '@/store/locale'
    17 | import { usePwa } from '@/plugins/pwa'
    18 | import { useUserStore } from '@/store/user'

vuetify/packages/docs/src/main.ts:17

    15 | import { useI18n } from '@/plugins/i18n'
    16 | import { useLocaleStore } from '@/store/locale'
>   17 | import { usePwa } from '@/plugins/pwa'
    18 | import { useUserStore } from '@/store/user'
    19 | import { useVuetify } from '@/plugins/vuetify'

vuetify/packages/docs/src/main.ts:18

    16 | import { useLocaleStore } from '@/store/locale'
    17 | import { usePwa } from '@/plugins/pwa'
>   18 | import { useUserStore } from '@/store/user'
    19 | import { useVuetify } from '@/plugins/vuetify'
    20 | import { ViteSSG } from '@vuetify/vite-ssg'

vuetify/packages/docs/src/main.ts:19

    17 | import { usePwa } from '@/plugins/pwa'
    18 | import { useUserStore } from '@/store/user'
>   19 | import { useVuetify } from '@/plugins/vuetify'
    20 | import { ViteSSG } from '@vuetify/vite-ssg'
    21 | 

...and 1091 more

New issues flagged (7 issues)

S4123

vuetify/packages/docs/src/store/ads.ts:44

    42 | 
    43 |     const { objects = [] } = (
>   44 |       await bucket?.objects
    45 |         .find({ type: 'ads' })
    46 |         .props('slug,title,metadata')

vuetify/packages/docs/src/store/banners.ts:64

    62 |       try {
    63 |         const { objects = [] } = (
>   64 |           await bucket?.objects
    65 |             .find({
    66 |               type: 'banners',

vuetify/packages/docs/src/store/shopify.ts:53

    51 | 
    52 |       const { objects = [] } = (
>   53 |         await bucket?.objects
    54 |           .find({ type: 'products' })
    55 |           .props('slug,title,metadata')

vuetify/packages/docs/src/store/sponsors.ts:29

    27 |     const { bucket } = useCosmic<Sponsor>()
    28 |     const { objects = [] } = (
>   29 |       await bucket?.objects
    30 |         .find({ type: 'sponsors' })
    31 |         .props('slug,title,metadata')

S4623

vuetify/packages/vuetify/src/components/VList/VList.tsx:42

    40 |   const type = getPropertyFromItem(item, props.itemType, 'item')
    41 |   const title = isPrimitive(item) ? item : getPropertyFromItem(item, props.itemTitle)
>   42 |   const value = getPropertyFromItem(item, props.itemValue, undefined)
    43 |   const children = getPropertyFromItem(item, props.itemChildren)
    44 |   const itemProps = props.itemProps === true ? pick(item, ['children'])[1] : getPropertyFromItem(item, props.itemProps)

S6544

vuetify/packages/docs/src/plugins/pwa.ts:8

     6 | import type { PwaPlugin } from '@/types'
     7 | 
>    8 | export const usePwa: PwaPlugin = async ({ isClient, router }) => {
     9 |   if (!isClient) return
    10 | 

S6551

vuetify/packages/api-generator/src/helpers/text.ts:18

    16 | }
    17 | 
>   18 | export const pascalize = (str: string) => str.split('-').map(capitalize).join('')
    19 | 
📋 View full report

Code no longer flagged (1101)

S4328

Copy link
Contributor

@francois-mora-sonarsource francois-mora-sonarsource left a comment

Choose a reason for hiding this comment

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

LGTM

@francois-mora-sonarsource
Copy link
Contributor

Quality Gate failed Quality Gate failed

Failed conditions 1 New issue

See analysis details on SonarQube

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE SonarQube for IDE

See this PR to fix the false positive #6463

this.foundLookupTsConfigs.sort();
// Sort property tsconfigs by their position in the user-provided list to preserve
// the intentional ordering from sonar.typescript.tsconfigPaths.
const providedOrder = this.providedPropertyTsConfigs?.map(p => p.path) ?? [];
Copy link
Contributor

Choose a reason for hiding this comment

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

it's a corner case, but if the user provided a glob instead of a path, this will not apply

@zglicz
Copy link
Contributor Author

zglicz commented Feb 27, 2026

Vuetify ruling differences

The sorting change produced ruling differences in the vuetify project, mainly in S4328 (imports). These are expected and were automatically resolved by the ruling bot.

Root cause: vuetify has a root tsconfig.json (no include/files — covers everything) plus sub-package tsconfigs (packages/docs/tsconfig.json, packages/vuetify/tsconfig.json, etc.). All extend the root and use lib: ["esnext"], so lib differences are not the issue here.

Before the fix, filesystem traversal could process the root tsconfig first, meaning ALL files were analyzed as one big program. After sorting alphabetically, sub-package tsconfigs are processed first (packages/api-generator/ → packages/docs/ → packages/vuetify/ → root), so files get analyzed in
the context of their actual package tsconfig.

With alphabetical sorting, src/tsconfig.json is processed before the
root tsconfig.json. Since src/tsconfig.json has baseUrl, it correctly
resolves modules and finds the issue — fixing a previous false negative.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zglicz zglicz force-pushed the fix/tsconfig-deterministic-order branch from 288d41a to f981a4b Compare February 27, 2026 09:25
@zglicz zglicz enabled auto-merge (squash) February 27, 2026 09:39
@sonarqube-next
Copy link

@zglicz zglicz merged commit 4a2116a into master Feb 27, 2026
39 checks passed
@zglicz zglicz deleted the fix/tsconfig-deterministic-order branch February 27, 2026 09:48
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.

3 participants