Skip to content

Conversation

@CyberT33N
Copy link

This pull request teaches the @typescript-eslint/typescript-estree project-service integration to recognize virtual processor child paths (for example docs/example.md/0.ts or file.mdc/0_0.ts) and automatically treat them as eligible for the TypeScript default project, enabling typed linting for code blocks extracted by processors such as @eslint/markdown.

Motivation

Typed linting for code extracted by processors (Markdown/MDX/.mdc fenced TS blocks) previously failed with errors like:

<path>/file.mdc/0_0.ts was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.

because the TypeScript project service only understood real filesystem paths, whereas ESLint processors only expose virtual child paths (e.g. file.mdc/0_0.ts) that do not exist on disk and cannot be added to tsconfig.json.

My original attempt to solve this in ESLint core and @eslint/markdown was to introduce a physicalFilename concept and to materialize temporary .ts files on disk (see eslint/eslint#20378, eslint/eslint#20375, and eslint/markdown#595). As discussed in the RFC, that pushed responsibility for creating and cleaning up temp files into every processor.

In the review discussion, @DMartens pointed out that:

I also do not think it should be the responsibility of the processor to create and delete files as that would require each processor to do it and it is error-prone (e.g. no cleanup when there is an error).
The main motivation for this change is that TypeScript types work which should be possible with the TypeScript Compiler API.

and suggested instead that the TypeScript integration itself (i.e. @typescript-eslint/parser / typescript-estree via the Compiler API / project service) should be made aware of virtual child paths.

This PR implements that suggestion in typescript-estree: virtual child paths nested under a real file on disk are detected and automatically allowed to use the default project, without requiring processors to materialize temp files or ESLint core to grow a physicalFilename API.

Type of Change

  • [] 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🔧 Refactoring (no functional changes, no api changes)

Detailed Changes

  • packages/typescript-estree/src/useProgramFromProjectService.ts:

    • Introduced a new helper isVirtualFilePath(filePathAbsolute: string): boolean that:
      • Immediately returns false if the exact filePathAbsolute exists on disk.
      • Otherwise walks up the path towards the filesystem root and checks ancestors with fs.statSync.
      • Treats the path as virtual if it finds a parent that is an actual file, which matches processor-style layouts such as:
        • <project>/docs/example.md/0.ts
        • <project>/rules/EXAMPLE.mdc/0_0.ts
      • Falls back to false on any unexpected error, preserving robustness.
    • Adjusted the project-service decision logic in useProgramFromProjectService:
      • Previously:
        • isDefaultProjectAllowed was derived solely from allowDefaultProject globs.
        • Virtual child paths like file.mdc/0_0.ts were not recognized and thus caused “not found by the project service” errors unless manually allow-listed.
      • Now:
        • isDefaultProjectAllowed is computed as:
          const isVirtualFile = isVirtualFilePath(filePathAbsolute);
          
          const isDefaultProjectAllowed =
            isVirtualFile ||
            filePathMatchedBy(filePathRelative, serviceAndSettings.allowDefaultProject);
        • This means:
          • Real files behave exactly as before and still rely on allowDefaultProject.
          • Virtual children under a real file are implicitly allowed to use the default project without additional configuration.
    • Left all existing semantics for:
      • Non-standard extensions (DEFAULT_EXTRA_FILE_EXTENSIONS / extraFileExtensions checks).
      • Default-project file count enforcement and error messages.
      • Reload behavior (reloadProjects) and singleRun handling.
    • The overall behavior change is narrowly scoped to virtual child paths that are:
      • Not present on disk, but
      • Have a real-file ancestor in the path.
  • packages/typescript-estree/tests/lib/useProgramFromProgramService.test.ts:

    • Added a focused unit test:

      it('treats virtual child paths under an existing file as allowed by the default project', () => {
        const { service } = createMockProjectService();
        const program = { getSourceFile: vi.fn() };
      
        const virtualFilePath = `path/docs/example.md/0.ts`;
      
        mockGetProgram.mockReturnValueOnce(program);
        mockCreateProjectProgram.mockReturnValueOnce(program);
      
        // Simulate that the project service does not associate the virtual path with a specific tsconfig.
        service.openClientFile.mockReturnValueOnce({});
      
        const filePathAbsolute = path.normalize(`${currentDirectory}/${virtualFilePath}`);
        const parentMarkdownPath = path.normalize(
          `${currentDirectory}/path/docs/example.md`,
        );
      
        const existsSpy = vi.spyOn(fs, 'existsSync');
        const statSpy = vi.spyOn(fs, 'statSync');
      
        existsSpy.mockImplementation(p => {
          const normalized = path.normalize(String(p));
          if (normalized === filePathAbsolute) {
            // The virtual child itself does not exist on disk.
            return false;
          }
          return false;
        });
      
        statSpy.mockImplementation(p => {
          const normalized = path.normalize(String(p));
      
          if (normalized === parentMarkdownPath) {
            // Pretend the parent Markdown file exists on disk.
            return {
              isFile: () => true,
            } as unknown as fs.Stats;
          }
      
          // Simulate a missing path for all other ancestors.
          const error = new Error('ENOENT');
          // @ts-expect-error - augmenting error for realism; not important to the test
          error.code = 'ENOENT';
          throw error;
        });
      
        const actual = useProgramFromProjectService(
          createProjectServiceSettings({
            // No allowDefaultProject globs: virtual children should still be allowed.
            allowDefaultProject: [],
            service,
          }),
          {
            ...mockParseSettings,
            filePath: virtualFilePath,
          },
          true,
          new Set(),
        );
      
        expect(actual).toBe(program);
      
        existsSpy.mockRestore();
        statSpy.mockRestore();
      });
    • This test explicitly verifies the new behavior:

      • With no allowDefaultProject globs configured.
      • For a virtual child path path/docs/example.md/0.ts whose parent path/docs/example.md is treated as a real file.
      • The call to useProgramFromProjectService returns the program from the project service instead of throwing a “not found by the project service” error.
    • Existing tests around:

      • allowDefaultProject glob handling.
      • Non-standard extensions and extraFileExtensions.
      • Default-project matching limits.
      • setHostConfiguration behavior.
        remain unchanged and continue to validate the original behaviors.

Testing & Verification

  • Unit Tests added/passed
    • New test in tests/lib/useProgramFromProjectService.test.ts for virtual child paths under an existing file.
    • Existing useProgramFromProjectService test suite continues to pass.
  • Manual verification steps:
    • Integrate a processor such as @eslint/markdown that produces virtual child paths (file.md/0.ts, file.mdc/0_0.ts).
    • Configure @typescript-eslint/parser with parserOptions.projectService: true.
    • Run ESLint on a Markdown / MDX / .mdc file containing TypeScript code blocks.
    • Observe that:
      • Type-aware rules now run successfully on code blocks.
      • The project service no longer reports “was not found by the project service” for virtual child paths.
      • Diagnostics continue to report the virtual child filename (e.g. file.mdc/0_0.ts), preserving existing UX.

Breaking Changes (if any)

N/A

This change is designed to be backwards compatible:

  • Only non-existent paths that are nested under an existing file are treated as virtual children and implicitly allowed to use the default project.
  • Real files on disk and existing allowDefaultProject behavior are unchanged.
  • Processors and configs that do not rely on virtual child paths (or do not use projectService) continue to behave as before.

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @CyberT33N!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint.

@netlify
Copy link

netlify bot commented Dec 8, 2025

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 3d44e5d
🔍 Latest deploy log https://app.netlify.com/projects/typescript-eslint/deploys/69362c4810a2100008d91d3a
😎 Deploy Preview https://deploy-preview-11827--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 80 (🔴 down 15 from production)
Accessibility: 97 (no change from production)
Best Practices: 100 (no change from production)
SEO: 92 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud
Copy link

nx-cloud bot commented Dec 8, 2025

View your CI Pipeline Execution ↗ for commit 3d44e5d

Command Status Duration Result
nx run-many -t lint ✅ Succeeded 3m 11s View ↗
nx run-many -t typecheck ✅ Succeeded 2m View ↗
nx test typescript-estree --coverage=false ✅ Succeeded 20s View ↗
nx run integration-tests:test ✅ Succeeded 5s View ↗
nx test eslint-plugin-internal --coverage=false ✅ Succeeded 3s View ↗
nx test eslint-plugin --coverage=false ✅ Succeeded 3s View ↗
nx run types:build ✅ Succeeded 2s View ↗
nx run generate-configs ✅ Succeeded 6s View ↗
Additional runs (29) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2025-12-08 01:47:11 UTC

@codecov
Copy link

codecov bot commented Dec 8, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.54%. Comparing base (32b7e89) to head (3d44e5d).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #11827   +/-   ##
=======================================
  Coverage   90.53%   90.54%           
=======================================
  Files         523      523           
  Lines       53096    53122   +26     
  Branches     8838     8851   +13     
=======================================
+ Hits        48073    48099   +26     
  Misses       5010     5010           
  Partials       13       13           
Flag Coverage Δ
unittest 90.54% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...escript-estree/src/useProgramFromProjectService.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CyberT33N CyberT33N force-pushed the feat/ts-compiler-api-virtual-files-for-processor-children/main branch from cea94af to 3d44e5d Compare December 8, 2025 01:39
@JoshuaKGoldberg JoshuaKGoldberg marked this pull request as draft December 10, 2025 16:53
@JoshuaKGoldberg
Copy link
Member

Hey thanks for the PR @CyberT33N! You've clearly put a lot of work + thought into this, and it's impressive to see a first PR touch some of the most technically intricate parts of the codebase (typed linting + virtual files).

Per the Contributing > Pull Requests guide linked in the PR template that you cleared, we ask that non-trivial PRs only be sent for open, accepted issues. This way we have a chance to discuss the design and desirability of changes before authors like you spend a lot of time implementing.

I'll also note that this PR body is very verbose. We have a very limited maintainer bandwidth and deeply reading through the whole thing would take up an unnecessarily large amount of time in it. We're working on docs in #11416 -> #11836. The tl;dr is: regardless of whether you use AI, excessively long descriptions actually harm understandability. Pithiness is good.

Please file an ✨ Enhance Another Package issue and fill out the template there so we can have that discussion. I've switched this PR to a draft in the meantime, to remove it from our review queue.


To be clear, I'm not saying I/we are against fixing the root issue around virtual children. I've actually hit this problem a lot in my own repos that use both @eslint/markdown and typed linting. I'm saying that we have good + necessary process points that we need to follow to make sure we're solving the issue in the right way.

Cheers!

@CyberT33N
Copy link
Author

Hey thanks for the PR @CyberT33N! You've clearly put a lot of work + thought into this, and it's impressive to see a first PR touch some of the most technically intricate parts of the codebase (typed linting + virtual files).

Per the Contributing > Pull Requests guide linked in the PR template that you cleared, we ask that non-trivial PRs only be sent for open, accepted issues. This way we have a chance to discuss the design and desirability of changes before authors like you spend a lot of time implementing.

I'll also note that this PR body is very verbose. We have a very limited maintainer bandwidth and deeply reading through the whole thing would take up an unnecessarily large amount of time in it. We're working on docs in #11416 -> #11836. The tl;dr is: regardless of whether you use AI, excessively long descriptions actually harm understandability. Pithiness is good.

Please file an ✨ Enhance Another Package issue and fill out the template there so we can have that discussion. I've switched this PR to a draft in the meantime, to remove it from our review queue.

To be clear, I'm not saying I/we are against fixing the root issue around virtual children. I've actually hit this problem a lot in my own repos that use both @eslint/markdown and typed linting. I'm saying that we have good + necessary process points that we need to follow to make sure we're solving the issue in the right way.

Cheers!

Thank you very much for taking the time to write such a detailed and kind reply, and for pointing me to the correct contributing / PR process. I now understand that I should first open an ✨ Enhance Another Package issue and only then follow up with a PR, and I’ve done so for this change. I also really appreciate that you’d already been thinking about this area in the context of @eslint/markdown – that background was very helpful to me.

@JoshuaKGoldberg
Copy link
Member

Closing per #11838 (comment).

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