Skip to content

feat: add architectural lint rules#39

Open
AlbertSmit wants to merge 5 commits into
mainfrom
feature/architecture-rules
Open

feat: add architectural lint rules#39
AlbertSmit wants to merge 5 commits into
mainfrom
feature/architecture-rules

Conversation

@AlbertSmit

@AlbertSmit AlbertSmit commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Cherry-picked from feature/no-duplicate-code — just the architecture rules, without the duplicate code engine (superseded by the semantic clone engine).

Rules added

no-restricted-layer-import

Enforce architectural layer boundaries — prevents imports that cross forbidden layer boundaries (e.g. machinery/ importing from features/).

no-nested-component

Prevent specific React components from being nested inside each other (e.g. Container inside Container, Heading inside Heading).

no-universal-in-universal

Disallow rendering universal components inside other universal files — universal components create separate hydration boundaries and must not be nested.

What's NOT included

The no-duplicate-code rule and its 400-line text-based engine have been dropped — the semantic clone engine (semantic-clone-research) fully supersedes it with Type 1–4 detection via AI embeddings + a VS Code extension.

Summary by CodeRabbit

  • New Features
    • Added an architecture-focused lint preset with new checks for import boundary violations, disallowed nested React component patterns, and “universal” component nesting.
    • Improved detection of component ancestry and source-layer context to support more precise linting.
  • Documentation
    • Documented the new lint rules and their configuration options.
  • Tests
    • Added comprehensive test coverage for the new preset rules and import-graph behavior.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds three ESLint rules, shared JSX and filename helpers, rule docs and tests, and a new plugin architecture config that registers the rules.

Changes

Architecture rule set

Layer / File(s) Summary
Shared JSX, filename, and import graph helpers
machinery/ast.js, machinery/filename.js, machinery/importGraph.js, machinery/importGraph.test.js
Exports JSX ancestor helpers, filename utilities for universal-file and layer detection, and an import-graph loader with graph queries and cache reset support.
Nested component rule
rules/no-nested-component/*
Adds the JSX nesting rule with default deny patterns, wildcard matching, and matching docs/tests.
Layer import restriction rule
rules/no-restricted-layer-import/*
Adds absolute import and export layer checks with default allow-lists, layer lookup helpers, and matching docs/tests.
Universal-in-universal rule
rules/no-universal-in-universal/*
Adds the .universal.js render guard, imported universal component tracking, transitive import-graph checks, and matching docs/tests.
Plugin rule registration
index.js
Registers the three new rules and adds plugin.configs.architecture with their enabled severities and nested-component deny list.

Sequence Diagram(s)

sequenceDiagram
  participant noUniversalRule
  participant getImportGraph
  participant importGraph
  participant contextReport
  noUniversalRule->>getImportGraph: load server-metafile.json
  getImportGraph->>importGraph: transitiveDeps(filePath)
  getImportGraph->>importGraph: findChain(source, target)
  noUniversalRule->>contextReport: report transitive universal nesting
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tsharms
  • guid-ooo
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.87% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding new architectural ESLint rules to the plugin.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/architecture-rules
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/architecture-rules

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
rules/no-universal-in-universal/readme.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
rules/no-restricted-layer-import/test.js (1)

4-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add export-path coverage for this rule.

Line 4-99 currently validates only import paths, but the rule also enforces export ... from and export * from. Add valid/invalid cases for both export forms to prevent regressions in those visitors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rules/no-restricted-layer-import/test.js` around lines 4 - 99, The
no-restricted-layer-import tests only cover import statements, so add coverage
in the test('no-restricted-layer-import') cases for both export-from forms
handled by the rule. Update the valid and invalid arrays with examples using the
same layer combinations already present, but expressed as export ... from and
export * from, so the visitors in the rule are exercised and regressions are
caught. Use the existing messages['restricted layer import'] expectations and
the same path/layer examples to keep the new cases aligned with the current rule
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rules/no-universal-in-universal/index.js`:
- Line 27: The universal import check in no-universal-in-universal/index.js only
recognizes specifiers ending with .universal, so .universal.js imports are
skipped. Update the source extension guard in the rule logic to also match
.universal.js (and any equivalent universal JS specifier pattern) before the
nested-universal validation runs, keeping the change localized to the import
detection path around the existing endsWith check.
- Around line 36-38: The JSX member-name check in the matching logic is using
the wrong part of the expression for namespace imports. Update the handling in
the JSX element name extraction so the comparison for JSXMemberExpression uses
the namespace/object identifier from the member expression rather than the
property name, and ensure the matching path in this rule correctly recognizes
imports like UI in <UI.Inner />.

In `@rules/no-universal-in-universal/test.js`:
- Around line 22-35: Add a failing test case in the no-universal-in-universal
rule spec to cover namespace imports from a .universal module, using the
existing invalid array in test.js and the messages['universal in universal']
assertion. The new case should import the universal module as a namespace (for
example via ImportNamespaceSpecifier) and render a nested member like
Universal.Inner in Outer.universal.js, so the rule’s import resolution logic is
exercised for member expressions as well as default/named imports.

---

Nitpick comments:
In `@rules/no-restricted-layer-import/test.js`:
- Around line 4-99: The no-restricted-layer-import tests only cover import
statements, so add coverage in the test('no-restricted-layer-import') cases for
both export-from forms handled by the rule. Update the valid and invalid arrays
with examples using the same layer combinations already present, but expressed
as export ... from and export * from, so the visitors in the rule are exercised
and regressions are caught. Use the existing messages['restricted layer import']
expectations and the same path/layer examples to keep the new cases aligned with
the current rule behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db075aa3-3394-4b98-ba1e-b45c98daeac1

📥 Commits

Reviewing files that changed from the base of the PR and between 313924b and 42aecf1.

📒 Files selected for processing (12)
  • index.js
  • machinery/ast.js
  • machinery/filename.js
  • rules/no-nested-component/index.js
  • rules/no-nested-component/readme.md
  • rules/no-nested-component/test.js
  • rules/no-restricted-layer-import/index.js
  • rules/no-restricted-layer-import/readme.md
  • rules/no-restricted-layer-import/test.js
  • rules/no-universal-in-universal/index.js
  • rules/no-universal-in-universal/readme.md
  • rules/no-universal-in-universal/test.js

return {
ImportDeclaration(node) {
const source = node.source.value
if (!source.endsWith('.universal')) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Universal import detection misses .universal.js specifiers.

Line 27 only matches .universal, so imports like /features/Foo.universal.js are silently skipped and nested-universal violations can pass unreported.

Proposed fix
-        if (!source.endsWith('.universal')) return
+        if (!source.endsWith('.universal') && !source.endsWith('.universal.js')) return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!source.endsWith('.universal')) return
if (!source.endsWith('.universal') && !source.endsWith('.universal.js')) return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rules/no-universal-in-universal/index.js` at line 27, The universal import
check in no-universal-in-universal/index.js only recognizes specifiers ending
with .universal, so .universal.js imports are skipped. Update the source
extension guard in the rule logic to also match .universal.js (and any
equivalent universal JS specifier pattern) before the nested-universal
validation runs, keeping the change localized to the import detection path
around the existing endsWith check.

Comment thread rules/no-universal-in-universal/index.js
Comment thread rules/no-universal-in-universal/test.js
Reads @kaliber/build's .kaliber-build/server-metafile.json to detect
.universal.js files that transitively depend on other .universal.js files
through intermediate regular files.

Gracefully degrades to direct-only checking if no metafile exists.
- 27 tests for machinery/importGraph.js covering lifecycle, circular deps,
  diamond deps, deep chains, external imports, and realistic Kaliber patterns
- Expanded rule tests: namespace imports, member expressions, multiple imports
- Fix: JSXMemberExpression now checks object name (the namespace) not property
  name, so <Inner.Component /> correctly flags 'Inner' from .universal import
@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm js-yaml is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/js-yaml@4.2.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/js-yaml@4.2.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

- layerFromSegments: shared segment→layer parsing (internal)
- getLayerFromImportPath: import path to layer name
- isSublayerOf: replaces raw startsWith(parent + '/') checks
- matchesLayerPattern: wildcard + parent matching for allow-lists
- isComponentName (ast.js): replaces inline char[0] uppercase checks
- no-restricted-layer-import: uses helpers, removed local duplicates
- no-nested-component: uses isComponentName from ast.js
- Expanded tests: 29 no-nested-component, 43 no-restricted-layer-import

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
rules/no-universal-in-universal/index.js (1)

54-56: 📐 Maintainability & Code Quality | 🔵 Trivial

Use context.cwd instead of deprecated context.getCwd()

context.getCwd() was deprecated in ESLint 9 and fully removed in ESLint 10. Since the codebase targets modern environments, replace the conditional check with the standard property access.

♻️ Suggested change
-        const cwd = context.getCwd ? context.getCwd() : process.cwd()
+        const cwd = context.cwd
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rules/no-universal-in-universal/index.js` around lines 54 - 56, The
Program:exit handler in no-universal-in-universal/index.js still uses the
deprecated context.getCwd() fallback. Update that logic to read the current
working directory from context.cwd directly in the Program:exit function, and
remove the conditional/deprecated API usage so getImportGraph(cwd) continues to
receive the resolved cwd value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@machinery/importGraph.js`:
- Around line 20-34: The module-level cache in getImportGraph is only keyed by
projectRoot, so changes to server-metafile.json are not picked up during
watch/LSP sessions. Update getImportGraph to also track the metafile’s mtime (or
equivalent freshness signal) alongside cachedRoot/cached, and only return the
cached parse when the file timestamp matches; otherwise reread and reparse the
metafile before storing the new cache.

In `@machinery/importGraph.test.js`:
- Around line 9-12: The afterEach cleanup in importGraph.test.js uses an empty
catch block around rmSync, which triggers ESLint no-empty and can fail CI if the
plugin lints itself. Update the catch in the test cleanup to explicitly
acknowledge the intentionally ignored error, or otherwise handle the ignored
error in a way that satisfies the lint rule while keeping the existing
_resetCache and rmSync fixture cleanup behavior.

In `@rules/no-universal-in-universal/index.js`:
- Around line 62-79: The transitive universal nesting check in the
no-universal-in-universal rule is using a path value that may contain Windows
backslashes, which prevents it from matching the esbuild graph keys. Update the
logic around the `relPath` lookup in `index.js` so the path is normalized to
forward slashes before calling the graph methods used by the
`transitiveDeps`/`findChain` flow, ensuring the `for (const dep of allDeps)`
check still reports violations consistently across platforms.

---

Nitpick comments:
In `@rules/no-universal-in-universal/index.js`:
- Around line 54-56: The Program:exit handler in
no-universal-in-universal/index.js still uses the deprecated context.getCwd()
fallback. Update that logic to read the current working directory from
context.cwd directly in the Program:exit function, and remove the
conditional/deprecated API usage so getImportGraph(cwd) continues to receive the
resolved cwd value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 383c17bc-8c22-4475-ba5a-085bb8a1aeb7

📥 Commits

Reviewing files that changed from the base of the PR and between 42aecf1 and 74104b3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • .gitignore
  • machinery/ast.js
  • machinery/filename.js
  • machinery/importGraph.js
  • machinery/importGraph.test.js
  • rules/no-nested-component/index.js
  • rules/no-nested-component/test.js
  • rules/no-restricted-layer-import/index.js
  • rules/no-restricted-layer-import/test.js
  • rules/no-universal-in-universal/index.js
  • rules/no-universal-in-universal/readme.md
  • rules/no-universal-in-universal/test.js
✅ Files skipped from review due to trivial changes (2)
  • rules/no-universal-in-universal/readme.md
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (4)
  • rules/no-universal-in-universal/test.js
  • machinery/filename.js
  • machinery/ast.js
  • rules/no-nested-component/index.js

Comment thread machinery/importGraph.js
Comment on lines +20 to +34
function getImportGraph(projectRoot) {
if (cached && cachedRoot === projectRoot) return cached

const metafilePath = join(projectRoot, '.kaliber-build', 'server-metafile.json')
if (!existsSync(metafilePath)) return null

try {
const metafile = JSON.parse(readFileSync(metafilePath, 'utf-8'))
cached = parseMetafile(metafile)
cachedRoot = projectRoot
return cached
} catch {
return null
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Module-level cache is never invalidated when the metafile changes.

The docstring states the metafile is rewritten during watch mode, but the cache is keyed only on projectRoot and persists for the lifetime of the process. In a long-lived ESLint session (editor/LSP or --watch), a rebuild that updates server-metafile.json will not be picked up, so transitive-nesting results in no-universal-in-universal can go stale until the process restarts.

Consider invalidating on file mtime.

♻️ Possible approach using mtime
-const { readFileSync, existsSync } = require('node:fs')
+const { readFileSync, existsSync, statSync } = require('node:fs')
 ...
 let cached = null
 let cachedRoot = null
+let cachedMtimeMs = null
 ...
 function getImportGraph(projectRoot) {
-  if (cached && cachedRoot === projectRoot) return cached
-
   const metafilePath = join(projectRoot, '.kaliber-build', 'server-metafile.json')
   if (!existsSync(metafilePath)) return null
+
+  const mtimeMs = statSync(metafilePath).mtimeMs
+  if (cached && cachedRoot === projectRoot && cachedMtimeMs === mtimeMs) return cached

   try {
     const metafile = JSON.parse(readFileSync(metafilePath, 'utf-8'))
     cached = parseMetafile(metafile)
     cachedRoot = projectRoot
+    cachedMtimeMs = mtimeMs
     return cached
   } catch {
     return null
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getImportGraph(projectRoot) {
if (cached && cachedRoot === projectRoot) return cached
const metafilePath = join(projectRoot, '.kaliber-build', 'server-metafile.json')
if (!existsSync(metafilePath)) return null
try {
const metafile = JSON.parse(readFileSync(metafilePath, 'utf-8'))
cached = parseMetafile(metafile)
cachedRoot = projectRoot
return cached
} catch {
return null
}
}
const { readFileSync, existsSync, statSync } = require('node:fs')
let cached = null
let cachedRoot = null
let cachedMtimeMs = null
function getImportGraph(projectRoot) {
const metafilePath = join(projectRoot, '.kaliber-build', 'server-metafile.json')
if (!existsSync(metafilePath)) return null
const mtimeMs = statSync(metafilePath).mtimeMs
if (cached && cachedRoot === projectRoot && cachedMtimeMs === mtimeMs) return cached
try {
const metafile = JSON.parse(readFileSync(metafilePath, 'utf-8'))
cached = parseMetafile(metafile)
cachedRoot = projectRoot
cachedMtimeMs = mtimeMs
return cached
} catch {
return null
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@machinery/importGraph.js` around lines 20 - 34, The module-level cache in
getImportGraph is only keyed by projectRoot, so changes to server-metafile.json
are not picked up during watch/LSP sessions. Update getImportGraph to also track
the metafile’s mtime (or equivalent freshness signal) alongside
cachedRoot/cached, and only return the cached parse when the file timestamp
matches; otherwise reread and reparse the metafile before storing the new cache.

Comment on lines +9 to +12
afterEach(() => {
_resetCache()
try { rmSync(fixtureRoot, { recursive: true, force: true }) } catch {}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Empty catch block flagged by ESLint (no-empty).

If the plugin lints its own sources, this [error] will fail CI. Add a comment (or destructure the ignored error) to satisfy the rule while keeping the intentional swallow.

🧹 Proposed fix
-  try { rmSync(fixtureRoot, { recursive: true, force: true }) } catch {}
+  try { rmSync(fixtureRoot, { recursive: true, force: true }) } catch { /* fixtures may not exist */ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterEach(() => {
_resetCache()
try { rmSync(fixtureRoot, { recursive: true, force: true }) } catch {}
})
afterEach(() => {
_resetCache()
try { rmSync(fixtureRoot, { recursive: true, force: true }) } catch { /* fixtures may not exist */ }
})
🧰 Tools
🪛 ESLint

[error] 11-11: Empty block statement.

(no-empty)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@machinery/importGraph.test.js` around lines 9 - 12, The afterEach cleanup in
importGraph.test.js uses an empty catch block around rmSync, which triggers
ESLint no-empty and can fail CI if the plugin lints itself. Update the catch in
the test cleanup to explicitly acknowledge the intentionally ignored error, or
otherwise handle the ignored error in a way that satisfies the lint rule while
keeping the existing _resetCache and rmSync fixture cleanup behavior.

Source: Linters/SAST tools

Comment on lines +62 to +79
for (const dep of allDeps) {
if (!dep.endsWith('.universal.js')) continue

// Skip if already caught by the direct check
const depWithoutExtension = dep.replace(/\.js$/, '')
const isDirectImport = [...directUniversalSources].some(
source => depWithoutExtension.endsWith(source.replace(/^\//, ''))
)
if (isDirectImport) continue

const chain = graph.findChain(relPath, dep)
if (!chain) continue

context.report({
message: messages['transitive universal nesting'](chain.join(' → ')),
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how esbuild metafile input keys are produced/consumed in this repo
fd -t f 'server-metafile' -H
rg -nP "inputs" -g '*.js' -C2 --iglob '*metafile*'
# Look for any normalization of separators around graph keys
rg -nP "(relative\(|replace\(/\\\\|sep)" rules/no-universal-in-universal machinery

Repository: kaliberjs/eslint-plugin

Length of output: 161


Fix Windows path separator mismatch in transitive dependency check.

On Windows, path.relative returns backslashes (\), but esbuild metafile keys (inputs) always use forward slashes (/). The current relPath derived from ESLint's context will not match the graph keys, causing transitiveDeps to return an empty set and silently missing violations.

Normalize relPath to use forward slashes before querying the graph:

-       const relPath = relative(cwd, context.filename)
+       const relPath = relative(cwd, context.filename).replace(/\\/g, '/')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rules/no-universal-in-universal/index.js` around lines 62 - 79, The
transitive universal nesting check in the no-universal-in-universal rule is
using a path value that may contain Windows backslashes, which prevents it from
matching the esbuild graph keys. Update the logic around the `relPath` lookup in
`index.js` so the path is normalized to forward slashes before calling the graph
methods used by the `transitiveDeps`/`findChain` flow, ensuring the `for (const
dep of allDeps)` check still reports violations consistently across platforms.

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