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

Implement the new RelativeRequireToLib cop + add tests #4

Merged
merged 14 commits into from Jul 23, 2020

Conversation

utkarsh2102
Copy link
Owner

@utkarsh2102 utkarsh2102 commented Jun 18, 2020

TODO:

  • document stuff.
  • fix regex to match proper calls. use File.expand_path to check if the expanded path lies inside the lib directory or not.
  • find a way to only check calls in the spec(s)/test(s) dir.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>

@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch from 90e4bfe to c08ac3a Compare June 18, 2020 13:23
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch 4 times, most recently from 2bdb52b to fdbb59f Compare June 23, 2020 05:25
.rubocop.yml Show resolved Hide resolved
lib/rubocop/cop/packaging/relative_require_to_lib.rb Outdated Show resolved Hide resolved
@deivid-rodriguez
Copy link
Collaborator

Rather than reviewing the specifics, I'll do a recap of the expectations to make sure we're all on the same page.

I see two "families of cases" we are interested in. One of them "solvable" and one of the "ambiguous".

When the require call does not depend on the CWD

This case is the one we can reliably solve without false positive. Examples:

  • require with an absolute path from a file inside specs that falls into lib. For example, require "/path/to/file/inside/lib/foo.rb". This case happens hardly ever in real life but we should be able to reduce more complex cases to this one.

  • require with a File.expand_path call based on __dir__ or __FILE__. For example, require File.expand_path("../lib/foo.rb", __dir__).

  • Any call to require_relative (which implicitly expands its argument from __dir__).

I believe to solve this case in general we need to be able to access the path to current file being inspected, since that will give us the real value of __dir__ and __FILE__.

I had a quick look at some rubocop cops that use this information, and we should be able to get this with:

def investigate(processed_source)
  we_need_this = processed_source.file_path
  # ...
end

When the require call depends on the CWD

Unfortunately, this case cannot be solved reliably, because the CWD is only known at runtime. For example, require "../lib/foo.rb" is an offense if the code is run from specs, but it's not an offense if the code is run from specs/foo.

These cases are pretty uncommon and involve any calls to require where the argument starts with a dot (.).

My proposal would be to give up on this for now, and if we have time, implement a more general cop that flags any calls to require that depend on the CWD. It should be a very easy cop anyways, just check if require is used, and the argument starts with ..

@terceiro
Copy link
Collaborator

thanks @deivid-rodriguez for the thoughts you put into this. for the "When the require call depends on the CWD" case, I think we should also emit an offense because people should not be doing this. it can even be exploited for security flaws.

@deivid-rodriguez
Copy link
Collaborator

Yeah, I think it's a good cop idea. But I believe this could be added upstream since it's not specific to packaging.

@deivid-rodriguez
Copy link
Collaborator

In any case, that case is super simple, just check that the require argument starts with a dot. The could be other cases, like require Pathname(<foo>).relative_path_from(<bar>), but that would pick most offenses.

@utkarsh2102 utkarsh2102 changed the title WIP: Implement the new RelativeRequireToLib cop + add tests Implement the new RelativeRequireToLib cop + add tests Jul 15, 2020
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch 2 times, most recently from 09630e0 to 76fd7fc Compare July 18, 2020 22:14
TODO: document stuff.
TODO: use expand_path to see if the expanded path is outside spec/.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
Previously, the AST pattern could just match simple
`require` calls with relative paths (via regex).
With this new fix, the pattern can correctly
find `require` calls in different formats, like:
- require File.expand_path('../lib/rubocop', __FILE__)
- require File.expand_path("../lib/rubocop", __dir__)
- $:.unshift("../lib")
  require '../lib/file.rb'
- require "#{path}/../lib/rubocop"
- require File.dirname(__FILE__) + '/../lib/rubocop'
- require File.dirname(__dir__) + '/../lib/rubocop'

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
Previously, only the `relative` calls with relative path
to lib directoy were being caught as an offense.
With this, even `relative_require` calls will now be
caught as an offense.

And also add its tests.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
Previously, both `require` and `require_relative` calls
were being inspected by the same cop.
This had various problems as catching all `require` calls
can be a little tricky and not as straight-forward as
`require_relative` ones. Hence, the split.

For now, the root directory is hardcoded.
And the specs of `require` calls have been filtered out.
Moreover, AST pattern has been fixed to just accomodate
the `require_relative` calls.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
Mostly because #delete_suffix and #delete_prefix
methods are not available in Ruby 2.4.x and were
added in Ruby 2.5.x.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
The tests were written wrt a single directory, "spec" and
a single file, "foo".
This diversifies these tests to include all possible
directories like "spec", "specs", "test", and "tests" and
all sorts of files like "foo", "bar", "baz", and "qux".

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
RuboCop::ConfigLoader.project_root correctly gets
the root directory of the path.

Thanks to @deivid-rodriguez.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch 2 times, most recently from 400318d to 3400dac Compare July 21, 2020 11:46
Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch 2 times, most recently from 6976559 to a093351 Compare July 21, 2020 12:26
Copy link
Collaborator

@terceiro terceiro left a comment

Choose a reason for hiding this comment

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

LGTM

@marcandre
Copy link
Contributor

marcandre commented Jul 21, 2020

FYI, I wrote this ruby-style-guide entry which I think goes exactly against this cop... Let me know if the justification isn't clear enough.

@deivid-rodriguez
Copy link
Collaborator

Hi @marcandre!

Thanks for your help here and for writing that guideline in the style guide. We're aware of that best practice and we do support that. As a matter of fact I've been migrating code over the years to follow that 👍.

With this cop, we don't intend to break that rule in general, only to add one exception to it. It turns out that for gem packagers it's useful to be able to "break" a gem into its different components (lib/, test/, exe/, and so on), in order to fit the different layouts in which gems can be installed as OS packages. So, when relative_require is used "across components", making assumptions on how the components will be installed relatively to each other, this sometimes forces gem packagers to monkeypatch gems to not do that, and instead rely on the $LOAD_PATH setup.

The cases we are preventing don't look great to me as relative_require's anyways, because you have to go several levels down, and then up again, which is not as straightforward as requiring stuff relatively inside the same subtree. And the performance gain doesn't seem like a big deal to me either. It's only test performance, not runtime performance, and I don't think there's usually enough of these requires so that this makes any noticiable difference in your tests.

A similar situation, but that affects not only packaging contexts, would be requiring your lib code from your gem executable relatively. That wouldn't work for default gems, because the layout of the installed gem doesn't match the development layout of the gem.

Anwyays, what I mean is that we don't intend to go against that rule in general, only to provide a tool that people interested in getting their gems more easily packaged for OSs can follow.

Rakefile Show resolved Hide resolved
config/default.yml Outdated Show resolved Hide resolved
lib/rubocop/cop/packaging/require_relative_to_lib.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/packaging/require_relative_to_lib.rb Outdated Show resolved Hide resolved
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch from a093351 to a1aa8c0 Compare July 22, 2020 16:22
with #starts_with_relative_path? as the naming
doesn't have anything to do with what the method
does.

Thanks, @deivid-rodriguez.

Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch from a1aa8c0 to ead6d30 Compare July 22, 2020 16:33
config/default.yml Outdated Show resolved Hide resolved
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch from ead6d30 to 7e27ea5 Compare July 22, 2020 16:38
Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch from 7e27ea5 to c7e27e5 Compare July 22, 2020 16:51
Signed-off-by: Utkarsh Gupta <utkarsh@debian.org>
Copy link
Collaborator

@deivid-rodriguez deivid-rodriguez left a comment

Choose a reason for hiding this comment

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

Nice work!

@utkarsh2102 utkarsh2102 force-pushed the add-relative_require_to_lib-cop branch from 8793331 to aababc1 Compare July 22, 2020 17:52
@deivid-rodriguez
Copy link
Collaborator

Also, since it might not be clear from my previous message, and from the cops implementation and naming. This cop only intends to detect requires from the specs folder to the lib folder. So, it should only ever be configured with

Include:
  - 'specs/**/*.rb'

or

Include:
  - 'test/**/*.rb'

Maybe we could validate the configuration of the cop to make sure it doesn't include any files inside lib, and rename the cop to something like RequireRelativeOfLibFileFromOutsideOfLibFolder, or something like that, to make this point more clear.

@utkarsh2102
Copy link
Owner Author

Thanks for the feedback so far! 😄
It looks like all these details could be provided via "good documentation" and perhaps that's what I am going to focus on after this.

I am going to merge this and carry on with the next tasks at hand, starting with documenting things properly! 🚀
(we can always keep discussing more about the guide and other things here..)

@utkarsh2102 utkarsh2102 added the enhancement New feature or request label Jul 23, 2020
@utkarsh2102 utkarsh2102 merged commit afa055d into master Jul 23, 2020
@utkarsh2102 utkarsh2102 deleted the add-relative_require_to_lib-cop branch July 23, 2020 16:48
@utkarsh2102 utkarsh2102 removed the enhancement New feature or request label Aug 20, 2020
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.

None yet

4 participants