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

feat(lint): new rule: noImportCycles #4948

Merged
merged 3 commits into from
Jan 24, 2025
Merged

Conversation

arendjr
Copy link
Contributor

@arendjr arendjr commented Jan 23, 2025

Summary

This PR adds a new rule: noImportCycles. Similar to the ESLint no-cycle rule: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md

Also closes #4930 , because this required exposing the DependencyGraph to lint rules.

Test Plan

Tests added.

@arendjr arendjr requested review from a team January 23, 2025 11:41
@arendjr arendjr changed the base branch from main to next January 23, 2025 11:41
@github-actions github-actions bot added A-CLI Area: CLI A-Linter Area: linter A-Changelog Area: changelog labels Jan 23, 2025
@github-actions github-actions bot added A-Project Area: project A-Parser Area: parser A-Tooling Area: internal tools L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis and removed A-Changelog Area: changelog labels Jan 23, 2025
Copy link

codspeed-hq bot commented Jan 23, 2025

CodSpeed Performance Report

Merging #4948 will not alter performance

Comparing arendjr:no-import-cycles (b33f6ed) with next (0bf4eaa)

Summary

✅ 95 untouched benchmarks

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Wooooow!! This is amazing! :)

I left some questions and feedback


impl Phase for DependencyGraphService {
fn phase() -> Phases {
Phases::Syntax
Copy link
Member

Choose a reason for hiding this comment

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

I believe this service should run for at least during semantic phase, or - even better IMHO - during a new phase after the semantic model.

I believe, in the future, we would want to pull the exported bindings from an imported module

E.g.

import { foo } from "../foo.js" // pull semantic module of `foo.js` so we can know if exports a `foo` binding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, early on I had a similar idea, but I'm starting to wonder if that's really the best way to go. After all, the analyzer phases are still based on the passes they make on an individual file. That's fine, but it doesn't really apply to data that gets populated by the scanner, because the scanner is responsible for making sure data of other files is there before the analyzer is even invoked.

So while I do agree we probably want to pull exported bindings as well, I think we'd do it in a similar "phase" as we currently use for discovering the import statement. But that's not actually an analyzer phase.

In the end I'm not really opinionated about which analyzer phase we should use. I chose Syntax because the data is already available at this point, but either is fine by me 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried changing to Phases::Semantic since it made more semantic sense (ghe-ghe), but it actually caused the rule to not work anymore. Didn't dive too deep, so I left it for now.

let result = ctx.imports_for_path(&resolved_path).and_then(|imports| {
let mut stack = stack.clone();
stack.push(resolved_path.to_string());
find_cycle(ctx, imports, seen, stack)
Copy link
Member

Choose a reason for hiding this comment

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

Any chance we can remove the recursion? I am a bit worried that this could cause a lot of slowdown and memory pressure in big projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'll give it a shot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

@@ -0,0 +1,31 @@
---
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a test where we have a cycle up to 4/5 order of depths? It would be nice to see how the diagnostic is rendered.

Comment on lines +120 to +121
std::env::current_dir()
.map(|cwd| cwd.to_string_lossy().to_string())
.unwrap_or_default(),
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this is correct, because the current working directory is now where the biome.json is found. Plus, current_dir might differ between CLI and LSP.

The use of ctx.file_path() might help here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I had some doubts myself what would be best here, but I think using something relative to the biome.json actually makes sense. After all, we want to strip a common base from the paths, in such a way that it is easy to recognize where they are. The only motivation is that using the full absolute path is just unnecessarily noisy for human readers.

ctx.file_path() doesn't really help with that, since the paths that get imported may be higher up in the hierarchy than the one from where we start, so what would the common base be then?

Copy link
Member

@ematipico ematipico Jan 24, 2025

Choose a reason for hiding this comment

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

Anyway, we could actually create an ad-hoc diagnostic/type that leverage the Location type, and it will resolve the paths once it's rendered at console (via biome_console::fmt::Display) by passing the working directory.

I can look into it if you want.

crates/biome_console/src/markup.rs Show resolved Hide resolved
crates/biome_js_analyze/src/lib.rs Show resolved Hide resolved
@arendjr arendjr merged commit b8c57d2 into biomejs:next Jan 24, 2025
11 checks passed
unvalley pushed a commit to unvalley/biome that referenced this pull request Jan 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Parser Area: parser A-Project Area: project A-Tooling Area: internal tools L-JavaScript Language: JavaScript and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

📎 Make DependencyGraph available to lint rules
2 participants