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

Add unused_import config options to require imports for each module used #3123

Merged
merged 19 commits into from
Feb 22, 2020

Conversation

jpsim
Copy link
Collaborator

@jpsim jpsim commented Feb 18, 2020

Continued from #3117

For example, if CGFloat is used in a file where only UIKit is imported but not CoreGraphics, this will be a violation even if the file previously compiled.

This is because Swift allows referencing some declarations that are only transitively imported.

Enabling the require_explicit_imports configuration option will require that the module of every declaration referenced in a source file be explicitly imported.

This will add significant noise to the imports list, but has a few advantages:

  1. It will be easier to understand all the dependencies explicitly referenced in a source file.
  2. Correcting the unused_import rule will no longer introduce compilation errors in files that compiled prior to the correction.

If missing imports are added to a file when correcting it, the sorted_imports rule will be automatically run on that file.

If you with to allow some imports to be implicitly importable transitively, you may specify the allowed_transitive_imports configuration:

unused_import:
  require_explicit_imports: true
  allowed_transitive_imports:
    - module: Foundation
      allowed_transitive_imports:
        - CoreFoundation
        - Darwin
        - ObjectiveC

For example, if `CGFloat` is used in a file where only `UIKit` is
imported but not `CoreGraphics`, this will be a violation even if the
file previously compiled.

This is because Swift allows referencing some declarations that are only
transitively imported.

Enabling the `require_explicit_imports` configuration option will
require that the module of every declaration referenced in a source file
be explicitly imported.

This will add significant noise to the imports list, but has a few
advantages:

1. It will be easier to understand all the dependencies explicitly
   referenced in a source file.
2. Correcting the `unused_import` rule will no longer introduce
   compilation errors in files that compiled prior to the correction.

Missing imports will always be added to the top of the file, which may
cause `sorted_imports` violations. You may run SwiftLint autocorrect
with the `sorted_imports` rule enabled to re-sort those import
statements afterwards.
@jpsim
Copy link
Collaborator Author

jpsim commented Feb 18, 2020

I want to add tests and a changelog entry before merging this.

@SwiftLintBot
Copy link

SwiftLintBot commented Feb 18, 2020

1 Warning
⚠️ Big PR
12 Messages
📖 Linting Aerial with this PR took 1.29s vs 1.27s on master (1% slower)
📖 Linting Alamofire with this PR took 2.14s vs 2.13s on master (0% slower)
📖 Linting Firefox with this PR took 9.0s vs 8.88s on master (1% slower)
📖 Linting Kickstarter with this PR took 13.78s vs 13.74s on master (0% slower)
📖 Linting Moya with this PR took 1.3s vs 1.25s on master (4% slower)
📖 Linting Nimble with this PR took 1.34s vs 1.35s on master (0% faster)
📖 Linting Quick with this PR took 0.53s vs 0.53s on master (0% slower)
📖 Linting Realm with this PR took 2.31s vs 2.31s on master (0% slower)
📖 Linting SourceKitten with this PR took 1.0s vs 1.0s on master (0% slower)
📖 Linting Sourcery with this PR took 6.59s vs 6.61s on master (0% faster)
📖 Linting Swift with this PR took 12.28s vs 12.34s on master (0% faster)
📖 Linting WordPress with this PR took 14.97s vs 14.91s on master (0% slower)

Generated by 🚫 Danger

@jpsim
Copy link
Collaborator Author

jpsim commented Feb 19, 2020

Oh Swift on Linux 🤦‍♂

error: use of undeclared type 'CFData'
typealias Foo = CFData
                ^~~~~~

will fix tomorrow

@jpsim
Copy link
Collaborator Author

jpsim commented Feb 19, 2020

@ZevEisenberg @marcelofabri @PaulTaykalo I thought you all might be interested in the enhancements to Example included in this PR.

We can now use the AutomaticTestableRule protocol & associated codegen with multiple configurations. Previously it was necessary to create new test cases manually when testing custom configurations for rules.

Copy link
Collaborator

@PaulTaykalo PaulTaykalo left a comment

Choose a reason for hiding this comment

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

LGTM

typealias Foo = CFData
""", configuration: [
"require_explicit_imports": true
])
Copy link
Collaborator

Choose a reason for hiding this comment

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

I already wrote it somewhere. there' s an idea to add a comment to the Example structure so that
it will explain why trigger should be triggered :) Totally out of the scope of this PR

Example("""
        ↓import Foundation
        typealias Foo = CFData
        """, configuration: [
            "require_explicit_imports": true
        ],comment: "Using transitive type 'CFData' from CoreFoundation should require CoreFoundation import")

This example is pretty obvious, but there are some examples that will help understanding why one or another rule should or should not be triggered.

also, this comment part can be shown in violation. I think additional PR will be needed :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah, that would be great for documentation purposes, but also to have nicer descriptions when tests fail.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Added. as an issue here.
#3125

/// Defaults to the file where this initializer is called.
/// - line: The line in the file where the example is located.
/// Defaults to the line where this initializer is called.
init(_ code: String, configuration: Any? = nil, testMultiByteOffsets: Bool = true, testOnLinux: Bool = true,
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great to add a test asserting that the two new parameters default to true unless otherwise specified.


/// Returns whether or not the file contains any attributes that require the Foundation module.
func containsAttributesRequiringFoundation() -> Bool {
guard contents.contains("@objc") else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this catch @objc in a comment? i.e. should this trigger?

// We don't need @objc because we're cool
@nonobjc func foo() {}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, this is just a fast path that skips more expensive checks further down.

}
A.dispatchMain()
↓import Dispatch

Copy link
Contributor

Choose a reason for hiding this comment

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

Extra newline intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it's a limitation of the rule right now that the import statement can't be at the very end of the file. If the file has a trailing newline, that's fine.

typealias Foo = CFData
""", configuration: [
"require_explicit_imports": true
], testMultiByteOffsets: false, testOnLinux: false):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it just a coincidence that testMultibyteOffsets and testOnLinux are always paired in this PR? Is there a scenario where you would want one but not the other?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just a coincidence.

static func dispatchMain() {}
}
A.dispatchMain()
"""): Example("""
Copy link
Contributor

Choose a reason for hiding this comment

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

If you put the values (as opposed to the keys) in this dictionary on their own line, they get indented, which to my eye is easier to read.

@@ -0,0 +1,53 @@
public struct TransitiveModuleConfiguration: Equatable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Docs on this type would be nice

.objc,
.objcName,
.objcMembers,
.objcNonLazyRealization
Copy link
Contributor

Choose a reason for hiding this comment

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

Not in scope for this PR, but I wanted to call it out: the lack of trailing commas on the last line of collection literals made #3040 harder, because I had to special-case a bunch of search and replace. I strongly recommend enforcing trailing commas in any collection-heavy codebase. Just my 2¢

@@ -3,22 +3,36 @@
public struct Example {
/// The contents of the example
public private(set) var code: String
/// The untyped configuration to apply to the rule, if deviating from the default configuration
public private(set) var configuration: Any?
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the configuration untyped? Might be nice to include something in the comma, so others understand the intended usage.

/// The untyped configuration to apply to the rule, if deviating from the default configuration
public private(set) var configuration: Any?
/// Whether the example should be tested by prepending multibyte grapheme clusters
public private(set) var testMultiByteOffsets: Bool
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not very clear from the comment what is actually meant. After reading it, I have several questions that I'd prefer not to have to go digging through the source code to answer:

  • why do multibyte grapheme clusters matter?
  • why might I want to disable them?
  • which multibyte clusters are tested? Are they specific or random?
  • where and when are the multibyte characters prepended, and what effect does that have?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why do multibyte grapheme clusters matter?

In the past, some rules were written with some assumptions that each character was one byte.

Characters that extend beyond a single byte encoded in UTF-8 need to be handled correctly.

The testMultiByteOffsets setting prepends some multi-byte grapheme clusters to the code in the example (specifically /* 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦 */) to ensure that the locations where violations are reported are correct, and that corrections are properly applied.

why might I want to disable them?

Some examples aren't resilient to prepending this content to its code and need to disable this part of the test. For example, this unused import code:

import Foundation
typealias Foo = CFData

Gets corrected by replacing import Foundation with import CoreFoundation:

import CoreFoundation
typealias Foo = CFData

However, when the violating example gets translated to this by the testMultiByteOffsets setting:

/* 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦 */
import Foundation
typealias Foo = CFData

then the correction becomes:

import CoreFoundation
/* 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦 */
typealias Foo = CFData

Which fails the test. There's nothing wrong with the rule's behavior, it just doesn't match what the simplistic auto-generated rule expects.

which multibyte clusters are tested? Are they specific or random?

Specific: /* 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦 */

where and when are the multibyte characters prepended, and what effect does that have?

See above.

static func dispatchMain() {}
}
A.dispatchMain()

Copy link
Contributor

Choose a reason for hiding this comment

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

Intentional whitespace? Here and above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Intentional, yes: #3123 (comment)

@jpsim jpsim merged commit d10ccac into master Feb 22, 2020
@jpsim jpsim deleted the missing-imports branch February 22, 2020 22:39
@AdrianBinDC
Copy link
Contributor

Awesome addition! Thank you for adding this.

optionalendeavors added a commit to optionalendeavors/SwiftLint that referenced this pull request Jul 12, 2020
* master: (101 commits)
  JUnit reporter for GitLab artifact:report:junit (realm#3177)
  Add empty changelog section
  release 0.39.2
  Update CI to run jobs with Xcode 11.0 to 11.4 (realm#3168)
  Fix false positives in valid_ibinspectable rule when using Swift 5.2 (realm#3155)
  Fix attributes rule false positive with Swift 5.2 (realm#3154)
  Fix CHANGELOG link
  Fix false positives in redundant_objc_attribute with Swift 5.2 (realm#3152)
  Fix false positives on implicit_getter with Swift 5.2+ (realm#3151)
  Simplify regex (realm#3145)
  fix links about configuration rules (realm#3142)
  Add unused_import config options to require imports for each module used (realm#3123)
  Add empty changelog section
  release 0.39.1
  Temporarily remove all SwiftSyntax rules and support (realm#3107)
  Fix unused_import rule reported locations and corrections (realm#3106)
  release 0.39.0
  Fix false positive in `empty_string` rule with multiline literals (realm#3101)
  Fix PrivateActionRule in Swift 5.2 (realm#3092)
  Fix false positive in implicit_getter with Swift 5.2 (realm#3099)
  ...

# Conflicts:
#	Source/SwiftLintFramework/Extensions/SwiftLintFile+Regex.swift
@mthole
Copy link

mthole commented Mar 17, 2022

(two years later)

I used this today on a large project. Thanks for this addition!

@jpsim
Copy link
Collaborator Author

jpsim commented Mar 18, 2022

I’m glad you found it useful! We run it daily on Lyft’s 1M+ lines of Swift.

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.

6 participants