Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

import/no-cycle does not detect some cycles with side-effect imports #3147

Open
guillaumebrunerie opened this issue Jan 20, 2025 · 19 comments
Open
Labels

Comments

@guillaumebrunerie
Copy link

guillaumebrunerie commented Jan 20, 2025

Here is the setup (https://codesandbox.io/p/devbox/my57j6):

// a.js
import "./b";
export const f = () => globalThis.x;

// b.js
import "./c";

// c.js
import { f } from "./a";

globalThis.x = f();

This seems like a cycle to me (a.js expects globalThis.x to be set given that it imports b.js which imports c.js which sets it, but c.js uses an import from a.js in order to set globalThis.x).
Yet this is not detected as a cycle by eslint-plugin-import. Note that changing a.js to import ./c instead of ./b is detected as a cycle, so it seems to be the indirection introduced by b.js which confuses no-cycle.

@ljharb
Copy link
Member

ljharb commented Jan 20, 2025

It’s not a cycle; it’s just a global variable (which you never want to be using).

@guillaumebrunerie
Copy link
Author

Well, a imports b which imports c which imports a. Isn’t that a cycle? The global variable is just there to show that it creates problems.
The actual problem I’m having is Webpack complaining about "cannot access variable before initialization", which in my experience is usually due to cycles, but import/no-cycle doesn’t report any error.
I now managed to fix my error by shuffling things around, but it still seems to me that import/no-cycle should report it.

@ljharb
Copy link
Member

ljharb commented Jan 20, 2025

Oh, fair point. Yes, that is a cycle. What's your config set to, and what version of the plugin are you using?

@guillaumebrunerie
Copy link
Author

Oh, fair point. Yes, that is a cycle. What's your config set to, and what version of the plugin are you using?

I’m using the latest version (2.31.0) with the latest version of ESLint (9.18.0) and the following config (taken mostly from the docs of eslint-plugin-import):

import importPlugin from "eslint-plugin-import";
import js from "@eslint/js";

export default [
  js.configs.recommended,
  importPlugin.flatConfigs.recommended,
  {
    files: ["**/*.{js,mjs,cjs}"],
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
    },
    rules: {
      "import/no-cycle": "warn",
      "import/default": "off",
    },
  },
  {
    ignores: ["dist"],
  },
];

@ljharb
Copy link
Member

ljharb commented Jan 20, 2025

ok, then it should be using scc. cc @soryy708

@soryy708
Copy link
Collaborator

Ok we're talking about a false-negative.
Lets check if the SCC optimization is responsible for it.
Try adding disableScc: true. Does no-cycle report it as a cycle then? or still no?

@soryy708
Copy link
Collaborator

I have looked in the attached codesandbox.
Why does it have both an .eslintrc.json and an .eslint.config.js?
Wouldn't that confuse ESLint?

Image

@guillaumebrunerie
Copy link
Author

Ok we're talking about a false-negative. Lets check if the SCC optimization is responsible for it. Try adding disableScc: true. Does no-cycle report it as a cycle then? or still no?

It makes no difference to add disableScc: true, the cycle still isn’t detected.

@guillaumebrunerie
Copy link
Author

I have looked in the attached codesandbox. Why does it have both an .eslintrc.json and an .eslint.config.js? Wouldn't that confuse ESLint?

Image

Hmm, not sure, I simply took some default codesandbox and added ESLint and eslint-plugin-import.
In my actual project, I do have both a eslint.config.js and a .eslintrc, but this is because the .eslintrc is needed for ignorePatterns to work properly with import/no-unused-modules.
Also, changing a.js to directly import ./c instead of ./b makes ESLint detect the cycle, so it does seem like ESLint is working properly.

@soryy708
Copy link
Collaborator

Cool.

In a.js:

import "./b.mjs";

is treated as a type-import,
we exit early in:

@ljharb
Copy link
Member

ljharb commented Jan 20, 2025

Why would that be a type import?

@soryy708
Copy link
Collaborator

I don't know. Probably shouldn't?

Perhaps because b.js has no exports?

@ljharb
Copy link
Member

ljharb commented Jan 21, 2025

It definitely shouldn’t - if something with no exports is imported, it’s for side effects, and that means it’s critical and must always be considered.

@guillaumebrunerie
Copy link
Author

we exit early in:

return; // ignore type imports

Looks like this also ignores imports of the form import { type Foo } from "foo", but it is worth noting that with the verbatimModuleSyntax TypeScript option, this form of import is used to import both a type and its side effects, so it should not be ignored.
Now I don’t know if it’s possible/desirable to have eslint-plugin-import behave differently depending on TypeScript options, but maybe only type imports of the form import type { Foo } from "foo" should be ignored by default? Or conversely there could be a verbatimModuleSyntax option for the import/no-cycle rule.

@ljharb
Copy link
Member

ljharb commented Jan 21, 2025

We do look at tsconfig for a flag already; I'm sure we could look up verbatimModuleSyntax.

@soryy708
Copy link
Collaborator

soryy708 commented Jan 21, 2025

So we have at least 3 false-negatives here:

  1. JS module style import '...';
  2. TS import type {...} from '...'; with verbatimModuleSyntax: true
  3. TS import { type ... } from '...'; with verbatimModuleSyntax: true

Would you like to contribute test cases?

@soryy708
Copy link
Collaborator

On another topic,

On one hand fixing a false-negative would technically be a bugfix (so perhaps "patch" in semver?),
but on the the other failing on what used to be fine before would technically be a breaking change (so perhaps "major" in semver?)

How will we want to handle these edge cases?

@ljharb
Copy link
Member

ljharb commented Jan 21, 2025

semver in eslint plugins is a bit tricky; producing more warnings can be a patch because it was a bug to miss them.

@soryy708 soryy708 added the bug label Jan 23, 2025
@soryy708
Copy link
Collaborator

apropos verbatimModuleSyntax:

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

No branches or pull requests

3 participants