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

Added ExplicitSelfRule #829

Closed
wants to merge 17 commits into from
Closed

Added ExplicitSelfRule #829

wants to merge 17 commits into from

Conversation

IanKeen
Copy link

@IanKeen IanKeen commented Oct 7, 2016

Requires the explicit use of self. when accessing instance members
Issue: #321

@IanKeen
Copy link
Author

IanKeen commented Oct 7, 2016

Not sure what to do with those failing tests? - looks like its swiftlint integration tests?

@scottrhoyt
Copy link
Contributor

I’m away from a computer right now, so I can’t be sure. But usually that means that the swiftlint code is generating errors or warnings while being linted by swiftlint itself. Try linting your local copy to see if this is the case.

On Oct 7, 2016, at 1:03 PM, Ian Keen notifications@github.com wrote:

Not sure what to do with those failing tests? - looks like its swiftlint integration tests?


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub #829 (comment), or mute the thread https://github.com/notifications/unsubscribe-auth/AED9suZlCUAgHhyR9cMkbg5Pdqj74P0eks5qxqWFgaJpZM4KRXxV.

@IanKeen
Copy link
Author

IanKeen commented Oct 7, 2016

Ahh you might be right - that trailing whitespace always gets me!

@codecov-io
Copy link

codecov-io commented Oct 7, 2016

Current coverage is 86.59% (diff: 100%)

Merging #829 into master will increase coverage by 0.58%

@@             master       #829   diff @@
==========================================
  Files           112        113     +1   
  Lines          4953       5162   +209   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           4260       4470   +210   
+ Misses          693        692     -1   
  Partials          0          0          

Powered by Codecov. Last update e65437c...f5ddc3a

Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

Awesome! I've been looking forward to this rule 👍


//get the members body (functions and calculated properties will have this data)
guard
let bodyLocation = (dictionary["key.bodyoffset"] as? Int64).flatMap({ Int($0) }),
Copy link
Contributor

Choose a reason for hiding this comment

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

You probably want as! Int64? so that this doesn't ignore wrong types.

Copy link
Author

Choose a reason for hiding this comment

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

🤔 as? seems to be the convention throughout - I think it would be preferential to a crash anyway

Copy link
Contributor

Choose a reason for hiding this comment

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

No, it would be preferable to crash over a silent bug. Here you just want to know if the value is not present.

Copy link
Author

Choose a reason for hiding this comment

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

I'm still inclined to leave it this way, every other rule is using code like this right now

//get the members body (functions and calculated properties will have this data)
guard
let bodyLocation = (dictionary["key.bodyoffset"] as? Int64).flatMap({ Int($0) }),
let bodyLength = (dictionary["key.bodylength"] as? Int64).flatMap({ Int($0) })
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you use flatMap(Int.init) here?

Copy link
Author

@IanKeen IanKeen Oct 8, 2016

Choose a reason for hiding this comment

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

Nope, It cant disambiguate (they are all .init(_ value: Foo))

"class Good2 { let value: Int = 42; func function() -> Int { return self.value } }",
"class Good3 { let value: Int; init() { self.value = 42 } }",
],
triggeringExamples: [
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome 👍

name: "Explict Self",
description: "Require explicit self for instance members",
nonTriggeringExamples: [
"class Good1 { let value: Int = 42; var property: Int { return self.value } }",
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add a few more cases here to verify that global variables, members of other types, etc. don't trigger violations.

Copy link
Contributor

Choose a reason for hiding this comment

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

And maybe same for accessing variables in a scope other than a type (like global values or local variables in a global function scope.
Also static functions! (Sorry, just thinking of more as I go hehe)

Copy link
Author

Choose a reason for hiding this comment

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

Keep them coming!! 👍

triggeringExamples: [
"class Bad1 { let value: Int = 42; var property: Int { return value } }",
"class Bad2 { let value: Int = 42; func function() -> Int { return value } }",
"class Bad3 { let value: Int; init() { value = 42 } }",
Copy link
Contributor

Choose a reason for hiding this comment

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

I can think of a few other cases:

  • Subclassing: accessing a member from the parent class.
  • struct and enum (because why not)
  • extension: verify that accessing a member from a function in an extension triggers this too.


func suffix(matches items: [[Generator.Element]]) -> Bool {
for test in items
where self.suffix(matches: test) { return true }
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be simpler using Array.contains, I think simply return items.contains(self.suffix)

Copy link
Author

@IanKeen IanKeen Oct 8, 2016

Choose a reason for hiding this comment

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

it calls the extension function (not the built in .suffix) above, this one is just a convenience to perform multiple checks at once

Copy link
Contributor

Choose a reason for hiding this comment

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

I know. You can use the appropriate function as the parameter to contas then. My point is that you don't need the loop, this is a common construct: return true if any element meets the criterion, which is what Array.contains does.

Copy link
Author

Choose a reason for hiding this comment

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

perhaps I'm misunderstanding what you mean? this function takes an Array<Array<Generator.Element>> and neither this function or the one it calls is equivalent to .contains - they are both checking the tail of the array for a set of elements in a specific order. can you perhaps show me a snippet of what you mean?

@keith
Copy link
Collaborator

keith commented Oct 8, 2016

Here's 2 interesting examples we might see that I'm not sure are covered here:

struct Foo {
    var a = 1
}

extension Foo {
    func bar() {
        print(a)
    }
}

Foo().bar()

and

struct Foo {
    var something = 1
}

extension Foo {
    func bar(something: Int) {
        print(something)
    }
}

Foo().bar(something: 1)

@IanKeen
Copy link
Author

IanKeen commented Oct 8, 2016

ah nice @keith ! @NachoSoto pointed out the first in his comments but the second will be an interesting one 😄

I'll dive into the suggestions this weekend at some point.

@norio-nomura norio-nomura added the enhancement Ideas for improvements of existing features and rules. label Oct 8, 2016
@IanKeen
Copy link
Author

IanKeen commented Oct 8, 2016

So I've significantly improved the test coverage thanks to your suggestions guys which has made the code much more robust 👌

The covered cases are (all tested with class and struct (except subclasses obviously)):

Member 👇 / Accessed From 👉 Init Function Calculated Property Extension Calculated Property Extension Function Static Property Static Function Subclass Property Subclass Function
Property
Function

as well as some edge cases:

  • Function parameter shadowing wont trigger this rule, eg:
class Edge1 { 
    let value: Int = 42
    func function(value: Int) -> Int { return value /* refers to parameter */ } 
}

class Edge2 { 
    func value() -> Int { return 42 }
    func function(value: Int) -> Int { return value /* refers to parameter */ } 
}

Thanks @NachoSoto and @keith for the suggestions 👍

+ "class Bad18_B: Bad18_A { func function() -> Int { return value() } }",
]

let nonTriggeringExamples = ["class", "struct"].flatMap(nonTriggering)
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@NachoSoto NachoSoto left a comment

Choose a reason for hiding this comment

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

Fantastic 👌

@NachoSoto
Copy link
Contributor

:shipit:

@keith
Copy link
Collaborator

keith commented Oct 10, 2016

I just ran this on our codebase and found a few false positives. Here's one:

struct Foo {
    var bar: Int

    func baz() {
        let bar = 123
        _ = bar
    }
}

@keith
Copy link
Collaborator

keith commented Oct 10, 2016

Here's another interesting one:

struct Foo {
    func bar() {
        self.foo.bar()
    }
}

@keith
Copy link
Collaborator

keith commented Oct 10, 2016

And another:

struct Foo {
    func baz() {
        let foo = 1
        foo.something()
    }
}

struct Qux {
    func something() {}
}

@keith
Copy link
Collaborator

keith commented Oct 10, 2016

I think some of these require more semantic understanding that what we have in SwiftLint. So maybe it isn't really feasible, but I can provide a few more examples if needed.

@IanKeen
Copy link
Author

IanKeen commented Oct 10, 2016

@keith hmm I can investigate that first one, but the others don't appear to be valid code? I'm not sure it's within the scope of a rule to lint code that otherwise doesn't compile?

@IanKeen
Copy link
Author

IanKeen commented Oct 10, 2016

@keith Updated to handle local scope shadowing
re: #829 (comment)

@keith
Copy link
Collaborator

keith commented Oct 11, 2016

Sorry, I didn't provide full enough examples here, but each of these cases was pulled from our real code and just made smaller for examples.

@IanKeen
Copy link
Author

IanKeen commented Oct 11, 2016

Ah right, well if you can turn examples 2 & 3 above into full working snippets I'm happy to take a look 😄

public init() { }

public static let description = RuleDescription(
identifier: "explicitSelf",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe rule identifier should be snake case.

@norio-nomura
Copy link
Collaborator

I tried to enable this rule in .swiftlint.yml of SwiftLint repository and ran integration test.
screenshot 2016-10-12 22 28 31

I feel it generates too much false positives yet.

public static let description = RuleDescription(
identifier: "explicitSelf",
name: "Explict Self",
description: "Require explicit self for instance members",
Copy link
Contributor

Choose a reason for hiding this comment

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

The wording seems weird. How about "Explicit self for instance members and functions is required".

@jpsim
Copy link
Collaborator

jpsim commented Nov 25, 2016

Given the number of false positives this triggers, I'm glad this is implemented as an opt-in rule.

@IanKeen would you mind rebasing this against master so we can make a last push to land this?

On a side note, this type of rule would benefit from being implemented in a new mode of SwiftLint that I've been considering: one that operates after the language Parse phase, actually after the Semantic Analysis phase, which would let SwiftLint know the difference between instance bar and local bar in @keith's comment above:

struct Foo {
    var bar: Int

    func baz() {
        let bar = 123
        _ = bar
    }
}

This type of mode would require that the module being analyzed 1) already be built by the compiler and 2) that SwiftLint would somehow know all the compiler flags needed to build the module. But if we can find a good user experience for this, it would enable much more powerful rules, from multi-file aware linting (e.g. #836) to static analysis.

This is much more straightforward for using with the Swift Package Manger since it generates a build manifest which includes all the compiler arguments (.build/debug.yaml), but it's doable with Xcode too.

The way jazzy does this is by straight up cleaning and building the Xcode project, parsing the xcodebuild log to extract arguments for a module, but that's very time consuming as the project has to be built from scratch to work reliably.

I hope I can sit down at one point and file an issue tracking exactly how building such a mode for SwiftLint could be built. Until then, I'm sharing these loose thoughts here to spur ideas among SwiftLint contributors and users.

@IanKeen
Copy link
Author

IanKeen commented Nov 25, 2016

@jpsim weird, should be good now

Copy link
Collaborator

@jpsim jpsim left a comment

Choose a reason for hiding this comment

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

I'd like to see someone that follows this style in their codebase run it against this rule before signing off... Maybe @NachoSoto?


extension SyntaxToken {
func name(file: File) -> String {
return file.contents.substring(self.offset, length: self.length)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Even though that's the opposite of what this rule is enforcing, this project prefers omitting self. wherever possible. Could you please adapt your code to be consistent with this?

.componentsSeparatedByString(",")

var signature = ""
for param in params {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this could be refactored as let signature = reduce("") { ... }

import Foundation
import SourceKittenFramework

func nonTriggering(type: String) -> [String] {
Copy link
Collaborator

Choose a reason for hiding this comment

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

all the implicitly internal members in this file that aren't needed in other files should be marked as private or fileprivate.

@NachoSoto
Copy link
Contributor

@jpsim I'll test this on Monday!

@jpsim
Copy link
Collaborator

jpsim commented Nov 29, 2016

@NachoSoto friendly reminder to test this when you get a chance 👍

@jpsim
Copy link
Collaborator

jpsim commented Dec 12, 2016

@IanKeen sorry for landing more changes recently and causing merge conflicts with this. Can you please rebase this so we can confirm it builds and passes tests on Linux?

Can anyone think of a popular OSS project that already implicitly adheres to this rule (in spirit at least) so that we can run this against a real codebase to make sure it works as intended, without too many false positives?

@JohnEstropia
Copy link
Contributor

JohnEstropia commented Jan 16, 2017

Can anyone think of a popular OSS project that already implicitly adheres to this rule

If you still need libraries to test this against, CoreStore observes this rule.

@kimdv
Copy link
Contributor

kimdv commented Mar 20, 2017

Any status on this @IanKeen ? 😄

@elland
Copy link

elland commented Apr 5, 2017

Can we expect this to be merged soon?

@sammy-SC
Copy link
Contributor

@IanKeen @jpsim
Is it alright if I try to continue the work on this PR?

@IanKeen
Copy link
Author

IanKeen commented Apr 18, 2017

ugh, very sorry I've had 0 time to jump back on this... I would love it if you wanna take this one @sammy-SC ... work + personal duties has left me no time for anything fun 😢

@sammy-SC
Copy link
Contributor

@IanKeen if you don't mind, could you give me push access to this branch? Otherwise I would have to open new PR.

@IanKeen
Copy link
Author

IanKeen commented Apr 26, 2017

@sammy-SC no idea how to do that, just added you as a collaborator on the repo 👍

@SwiftLintBot
Copy link

SwiftLintBot commented Apr 29, 2017

1 Error
🚫 Could not build branch
1 Warning
⚠️ Big PR

Generated by 🚫 danger

@sammy-SC
Copy link
Contributor

@IanKeen Thanks, that's exactly what I had in mind.

I tried running this rule against https://github.com/JohnEstropia/CoreStore, it produces lots of false negatives, I am looking into it.

@keith
Copy link
Collaborator

keith commented Apr 29, 2017

FWIW I've ran it on our codebase of ~70K LOC where we require self. and it generated about 4k false positives. For more context, running ag "\Wself\." shows that we have 12k self. uses.

It seems like a rule that needs this much context should really be run outside of SwiftLint against the AST, since otherwise it will never not produce significant numbers of false positives. Just my 2 cents.

@marcelofabri
Copy link
Collaborator

I'm closing this for now due to lack of activity and the high number of false positives. Feel free to open another pull request if the issues are fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Ideas for improvements of existing features and rules.
Projects
None yet
Development

Successfully merging this pull request may close these issues.