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(linter): add notDependOnLibsWithTags constraint option to enforce-module-boundaries rule #8633

Merged

Conversation

jaytavares
Copy link
Contributor

@jaytavares jaytavares commented Jan 21, 2022

Current Behavior

onlyDependOnLibsWithTags is the only option currently available for setting up tag-based constraints. See #984 for discussion of some limitations this presents

Expected Behavior

Addition of a notDependOnLibsWithTags option; provides the ability to prevent the import of a project that contains a disallowed tag.

Note: The nature of this option is a bit different than onlyDependOnLibsWithTags in that we really want to make sure a library tagged with a disallowed tag doesn't find its way into the source project. For this reason, I coded the implementation with a recursive search for the disallowed tags on all dependent projects.

Once I get some confirmation on the above approach, I can finish the remaining tasks:

TODO:

  • Add tslint implementation
  • Add project dependency path to report message (similar to circular dependency check message)
  • Document new constraint option

Related Issue(s)

Fixes #984

@vercel
Copy link

vercel bot commented Jan 21, 2022

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/nrwl/nx-dev/B7shm71yXh8gJHUaMcvcUJnRx5kA
✅ Preview: https://nx-dev-git-fork-curiovision-implement-not-depend-on-3b9843-nrwl.vercel.app

@jaytavares jaytavares changed the title Add notDependOnLibsWithTags constraint option to enforce-module-boundaries eslint rule Add notDependOnLibsWithTags constraint option to enforce-module-boundaries eslint rule Jan 21, 2022
@jaytavares jaytavares changed the title Add notDependOnLibsWithTags constraint option to enforce-module-boundaries eslint rule Add notDependOnLibsWithTags constraint option to enforce-module-boundaries lint rule Jan 21, 2022
@vsavkin
Copy link
Member

vsavkin commented Jan 28, 2022

@meeroslav can you take a look?

We need to make sure that:

  1. We don't introduce O(N^N) where N is the number of projects. Workspaces that have thousands of projects tend to have problems with it. We might want to do an extra traversal where we all the tags for all the projects, which will be O(N). Then nothing recursive will be required.
  2. Make sure it's documented.

@jaytavares
Copy link
Contributor Author

Fyi, I think failed test is leaking in from upstream code. I can't find a cause locally in this PR.

@meeroslav
Copy link
Contributor

meeroslav commented Feb 2, 2022

@jaytavares thank you for your PR.

I think the functionality makes sense, but there are a few things I would change:

  • Only one of onlyDependOnLibsWithTags and notDependOnLibsWithTags should exists at the time in the config. We need a strict check to make sure they are not both defined.
  • I don't think recursion is needed (nor look into depth). We should instead make sure that we are consistent in the restrictions. Look at the example below:

Let's say you have the following graph:

LIB A(`tag-Angular`) -\
                       \	
	                 LIB B(`tag-Shared`) ---- LIB C(`tag-React-Util`)
LIB D(`tag-React`) --- /

Let's say you want to set up the rule that tag-Angular cannot depend on the tag-React-Util, but tag-React can.
That would mean also that tag-Shared cannot depend on tag-React-Util otherwise tag-Angular wouldn't be able to depend on the tag-Shared.
In which case you would have to specify that tag-Shared also cannot depend on tag-React-Util and that makes the recursion unnecessary.

@meeroslav
Copy link
Contributor

Fyi, I think failed test is leaking in from upstream code. I can't find a cause locally in this PR.

Yeah, try rebasing on to the latest master. That should solve those E2E tests

@jaytavares jaytavares force-pushed the implement-not-depend-on-libs-with-tags branch from d952e11 to d0acaee Compare February 2, 2022 19:51
@jaytavares
Copy link
Contributor Author

  • Only one of onlyDependOnLibsWithTags and notDependOnLibsWithTags should exists at the time in the config. We need a strict check to make sure they are not both defined.

Are you thinking that we'd require separate config entries?

Like this:

"depConstraints": [
  {
    "sourceTag": "scope:tag-A",
    "onlyDependOnLibsWithTags": ["scope:tag-A"]
  },
  {
    "sourceTag": "scope:tag-A",
    "notDependOnLibsWithTags": [
      "scope:tag-B",
    ]
  }
]

Rather than a combined syntax like this:

"depConstraints": [
  {
    "sourceTag": "scope:tag-A",
    "onlyDependOnLibsWithTags": ["scope:tag-A"]
    "notDependOnLibsWithTags": [
      "scope:tag-B",
    ]
  }
]

@meeroslav
Copy link
Contributor

meeroslav commented Feb 3, 2022

Are you thinking that we'd require separate config entries?

What I meant was disallowing both. The rule should throw an error if it finds the same sourceTag defined with onlyDependOnLibsWithTags and notDependOnLibsWithTags regardless of whether it's in the same constraint block or in separate ones.

If it's ok with you I can add this code to your PR.

@meeroslav meeroslav added the scope: linter Issues related to Eslint support in Nx label Feb 3, 2022
@jaytavares
Copy link
Contributor Author

Hmm. Why do you think it's necessary to limit libraries to only one or the other? It seems like it would be more versatile to allow both.

Copy link
Contributor

@meeroslav meeroslav left a comment

Choose a reason for hiding this comment

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

I have a few minor changes. Also, don't forget to add the new property to documentation on https://github.com/nrwl/nx/blob/master/docs/shared/monorepo-tags.md.

If you want, you can skip the documentation and I will add it after your PR is merged.

Ignore the tslint as that linter is deprecated so we are not supporting it with new features anymore, only bug fixes.


if (found) return true;

for (let d of graph.dependencies[projectName] || []) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the recursion all look only at the first level of dependencies. We can always add matrix graphs, similar to circular dependencies if there is ever a need.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(More discussion needed, see PR conversation thread)

packages/workspace/src/utils/runtime-lint-utils.ts Outdated Show resolved Hide resolved
if (
constraint.notDependOnLibsWithTags &&
hasAnyOfTheseTags(
projectGraph,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not targetProject like on line 397?

Copy link
Contributor Author

@jaytavares jaytavares Feb 4, 2022

Choose a reason for hiding this comment

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

Because I don't have a full ProjectGraphNode object within the recursive call here.

If this call goes away, we can and should change this to match the similar function signature on line 397. I think there's still some discussion to be had regarding this though. (See PR conversation thread)

@meeroslav
Copy link
Contributor

meeroslav commented Feb 4, 2022

Hmm. Why do you think it's necessary to limit libraries to only one or the other? It seems like it would be more versatile to allow both.

Yes, sorry, I was thinking in a single dimension. With multiple tags per project, it makes sense to have both at the same time. Thanks for questioning my proposal.
My concern was that tags are already a too complex topic for some folks and adding new potentially overlapping property will only make it worse.

@jaytavares
Copy link
Contributor Author

jaytavares commented Feb 4, 2022

Let's say you have the following graph:

LIB A(`tag-Angular`) -\
                       \	
	                 LIB B(`tag-Shared`) ---- LIB C(`tag-React-Util`)
LIB D(`tag-React`) --- /

Let's say you want to set up the rule that tag-Angular cannot depend on the tag-React-Util, but tag-React can. That would mean also that tag-Shared cannot depend on tag-React-Util otherwise tag-Angular wouldn't be able to depend on the tag-Shared. In which case you would have to specify that tag-Shared also cannot depend on tag-React-Util and that makes the recursion unnecessary.

Hmm, I get what you're saying; however, I feel like this behavior would be less intuitive. (see below)

We should instead make sure that we are consistent in the restrictions.

With onlyDependOnLibsWithTags, the constraint will only pass if the given tag is added to the target lib. The presence of the tag on the target lib explicitly communicates that the library is suitable for use for the purposes of the tag. This is a failsafe condition.

The opposite is true for notDependOnLibsWithTags. It will only fail if the target has the given tag. If the author of the target library didn't realize they needed to apply a tag to their library simply because they imported from a problematic library, there will be no protection offered to consumers of their library. This is the opposite of failsafe.

It really does seem that we need deeper inspection for the latter since we lack the explicitness of the former. As for how we go about that inspection, I'm totally open to suggestions. I modeled my recursive function on the getPath() function used by the circular dependency check; however, I left the matrix stuff out since it was lacking comments explaining its purpose. I get the reasoning now. If you want, I can take a stab at the matrix approach.

@meeroslav
Copy link
Contributor

meeroslav commented Feb 10, 2022

The tooling should not compensate for the lack of good hygiene (setting proper tags wherever needed) with running extra checks. If the linter rule is slow, it will drive people to switch it off. That's why recursion is not an option.

The whole point of the matrix is to create structure once and keep it in memory, so we don't have to use recursion every time. Finding a path is then a straightforward process. This part of the linter rule is currently the most problematic one (see #8313) and will be redesigned soon.

Therefore my suggestion is to finish the PR without using the matrix (or any other in-depth checks). We can add this as an improvement with a follow-up PR.

Otherwise, feel free to look at the implementation of the matrix and try to re-use it.

@meeroslav
Copy link
Contributor

Hey @jaytavares, just checking in to see if you need help with finishing this PR?

The issue with the rule causing the mismatches in IDE is merged in, so you have a clear path to work on this.

I can add the function to search for descendants with the given tag using the getPath function.

@jaytavares
Copy link
Contributor Author

Hey @jaytavares, just checking in to see if you need help with finishing this PR?

The issue with the rule causing the mismatches in IDE is merged in, so you have a clear path to work on this.

I can add the function to search for descendants with the given tag using the getPath function.

Hey @meeroslav, sorry for the delayed response. I've been busy working on a work project. If you want to implement the search functionality using getPath() go for it! Otherwise, I can circle back to this once I've finished up my stuff in a week or two.

@meeroslav
Copy link
Contributor

@jaytavares, is it ok if I continue working on your branch?

Once this is merged in you would still be marked as author of the PR/commit.

@jaytavares
Copy link
Contributor Author

@jaytavares, is it ok if I continue working on your branch?

Once this is merged in you would still be marked as author of the PR/commit.

Sure, go for it.

@meeroslav
Copy link
Contributor

Please grant me access rights to your repo so I can rebase and force push master to this branch.

@jaytavares
Copy link
Contributor Author

Please grant me access rights to your repo so I can rebase and force push master to this branch.

Done

@meeroslav meeroslav force-pushed the implement-not-depend-on-libs-with-tags branch from 631e736 to a17dcc0 Compare March 4, 2022 15:00
@meeroslav meeroslav force-pushed the implement-not-depend-on-libs-with-tags branch from b99dc24 to c08b25b Compare March 4, 2022 15:02
@meeroslav meeroslav force-pushed the implement-not-depend-on-libs-with-tags branch from 2e6eda6 to 92ae03c Compare March 7, 2022 22:19
@meeroslav meeroslav changed the title Add notDependOnLibsWithTags constraint option to enforce-module-boundaries lint rule feat(linter): add notDependOnLibsWithTags constraint option to enforce-module-boundaries rule Mar 8, 2022
@meeroslav meeroslav merged commit 0a17a61 into nrwl:master Mar 8, 2022
@meeroslav meeroslav deleted the implement-not-depend-on-libs-with-tags branch March 8, 2022 10:55
@github-actions
Copy link

This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
scope: linter Issues related to Eslint support in Nx
Projects
None yet
Development

Successfully merging this pull request may close these issues.

add support for notDependOnLibsWithTags like onlyDependOnLibsWithTags
3 participants