Compare commits

...

2 Commits

Author SHA1 Message Date
JP Simard 01dfce3a5c
Pull SourceKitten from master 2020-01-01 12:22:30 -08:00
JP Simard 395f23db13
WIP Add OverexposedDeclarationRule 2020-01-01 10:11:31 -08:00
9 changed files with 301 additions and 24 deletions

View File

@ -33,8 +33,8 @@
"repositoryURL": "https://github.com/Quick/Nimble.git",
"state": {
"branch": null,
"revision": "6abeb3f5c03beba2b9e4dbe20886e773b5b629b6",
"version": "8.0.4"
"revision": "b02b00b30b6353632aa4a5fb6124f8147f7140c0",
"version": "8.0.5"
}
},
{
@ -50,9 +50,9 @@
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
"state": {
"branch": null,
"revision": "356551fc513eb12ed779bb369f79cf86a3a01599",
"version": "0.27.0"
"branch": "master",
"revision": "7e5bea0cb88f7e68739d1fe5252d2935048ea51a",
"version": null
}
},
{

View File

@ -15,7 +15,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/Carthage/Commandant.git", .upToNextMinor(from: "0.17.0")),
.package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.27.0"),
.package(url: "https://github.com/jpsim/SourceKitten.git", .branch("master")),
.package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.0.0"))] : []),

View File

@ -1,7 +1,7 @@
import Foundation
import SourceKittenFramework
public struct Location: CustomStringConvertible, Comparable, Codable {
public struct Location: Hashable, CustomStringConvertible, Comparable, Codable {
public let file: String?
public let line: Int?
public let character: Int?

View File

@ -112,6 +112,7 @@ public let masterRuleList = RuleList(rules: [
OpeningBraceRule.self,
OperatorFunctionWhitespaceRule.self,
OperatorUsageWhitespaceRule.self,
OverexposedDeclarationRule.self,
OverriddenSuperCallRule.self,
OverrideInExtensionRule.self,
PatternMatchingKeywordsRule.self,

View File

@ -0,0 +1,258 @@
import Foundation
import SourceKittenFramework
public struct OverexposedDeclarationRule: AutomaticTestableRule, ConfigurationProviderRule, AnalyzerRule,
CollectingRule {
public struct Declaration: Hashable {
let usr: String
let acl: AccessControlLevel
let location: Location
}
public struct Reference: Hashable {
let usr: String
let location: Location
}
public struct FileUSRs {
var referenced: Set<Reference>
var declared: Set<Declaration>
}
public typealias FileInfo = FileUSRs
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "overexposed_declaration",
name: "Overexposed Declaration",
description: "Declarations should be exposed to the lowest accessibility level needed to compile.",
kind: .lint,
nonTriggeringExamples: [
"""
private let kConstant = 0
_ = kConstant
""",
"""
private func foo() {}
foo()
"""
],
triggeringExamples: [
"""
let kConstant = 0
_ = kConstant
""",
"""
func foo() {}
foo()
"""
],
requiresFileOnDisk: true
)
public func collectInfo(for file: SwiftLintFile, compilerArguments: [String])
-> OverexposedDeclarationRule.FileUSRs {
guard !compilerArguments.isEmpty else {
queuedPrintError("""
Attempted to lint file at path '\(file.path ?? "...")' with the \
\(type(of: self).description.identifier) rule without any compiler arguments.
""")
return FileUSRs(referenced: [], declared: [])
}
let allCursorInfo = file.allCursorInfo(compilerArguments: compilerArguments)
return FileUSRs(referenced: Set(file.referencedUSRs(allCursorInfo: allCursorInfo)),
declared: Set(file.declaredUSRs(allCursorInfo: allCursorInfo)))
}
public func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: OverexposedDeclarationRule.FileUSRs],
compilerArguments: [String]) -> [StyleViolation] {
let allDeclarations = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.declared) }
let allReferences = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.referenced) }
let shouldBePrivateDeclarations = allDeclarations
.filter { $0.location.file == file.path }
.filter { $0.acl == .internal }
.filter { declaration in
return [declaration.location.file!] ==
Set(allReferences.filter({ $0.usr == declaration.usr }).map({ $0.location.file! }))
}
return shouldBePrivateDeclarations.map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: $0.location)
}
}
}
// MARK: - File Extensions
private extension SwiftLintFile {
func allCursorInfo(compilerArguments: [String]) -> [SourceKittenDictionary] {
guard let path = path,
let editorOpen = (try? Request.editorOpen(file: self.file).sendIfNotDisabled())
.map(SourceKittenDictionary.init) else {
return []
}
return syntaxMap.tokens
.compactMap { token in
guard let kind = token.kind, !syntaxKindsToSkip.contains(kind) else {
return nil
}
let offset = Int64(token.offset)
let request = Request.cursorInfo(file: path, offset: offset, arguments: compilerArguments)
guard var cursorInfo = try? request.sendIfNotDisabled() else {
return nil
}
if let acl = editorOpen.aclAtOffset(offset) {
cursorInfo["key.accessibility"] = acl.rawValue
}
cursorInfo["swiftlint.offset"] = offset
return cursorInfo
}
.map(SourceKittenDictionary.init)
}
func declaredUSRs(allCursorInfo: [SourceKittenDictionary]) -> [OverexposedDeclarationRule.Declaration] {
return allCursorInfo.compactMap { cursorInfo in
return declaredUSRPayload(cursorInfo: cursorInfo)
}
}
func referencedUSRs(allCursorInfo: [SourceKittenDictionary]) -> [OverexposedDeclarationRule.Reference] {
return allCursorInfo.compactMap(referencedUSR)
}
private func declaredUSRPayload(cursorInfo: SourceKittenDictionary) -> OverexposedDeclarationRule.Declaration? {
guard let offset = cursorInfo.swiftlintOffset ?? cursorInfo.offset,
let usr = cursorInfo.usr,
let kind = cursorInfo.declarationKind,
declarationKindsToLint.contains(kind),
let acl = cursorInfo.accessibility else {
return nil
}
// Skip declarations marked as @IBOutlet, @IBAction or @objc
// since those might not be referenced in code, but only dynamically (e.g. Interface Builder)
if let annotatedDecl = cursorInfo.annotatedDeclaration,
["@IBOutlet", "@IBAction", "@objc", "@IBInspectable"].contains(where: annotatedDecl.contains) {
return nil
}
// Classes marked as @UIApplicationMain are used by the operating system as the entry point into the app.
if let annotatedDecl = cursorInfo.annotatedDeclaration,
annotatedDecl.contains("@UIApplicationMain") {
return nil
}
// Skip declarations that override another. This works for both subclass overrides &
// protocol extension overrides.
if cursorInfo.value["key.overrides"] != nil {
return nil
}
// Sometimes default protocol implementations don't have `key.overrides` set but they do have
// `key.related_decls`.
if cursorInfo.value["key.related_decls"] != nil {
return nil
}
// Skip CodingKeys as they are used
if kind == .enum,
cursorInfo.name == "CodingKeys",
let annotatedDecl = cursorInfo.annotatedDeclaration,
annotatedDecl.contains("usr=\"s:s9CodingKeyP\">CodingKey<") {
return nil
}
// Skip XCTestCase subclasses as they are commonly not private
if kind == .class,
let annotatedDecl = cursorInfo.annotatedDeclaration,
annotatedDecl.contains("usr=\"c:objc(cs)XCTestCase\"") {
return nil
}
let location = Location(file: self, byteOffset: Int(offset))
return OverexposedDeclarationRule.Declaration(usr: usr, acl: acl, location: location)
}
private func referencedUSR(cursorInfo: SourceKittenDictionary) -> OverexposedDeclarationRule.Reference? {
guard let offset = cursorInfo.swiftlintOffset ?? cursorInfo.offset,
let usr = cursorInfo.usr,
let kind = cursorInfo.kind,
kind.starts(with: "source.lang.swift.ref") else {
return nil
}
let returnUSR: String
if let synthesizedLocation = usr.range(of: "::SYNTHESIZED::")?.lowerBound {
returnUSR = String(usr.prefix(upTo: synthesizedLocation))
} else {
returnUSR = usr
}
let location = Location(file: self, byteOffset: Int(offset))
return OverexposedDeclarationRule.Reference(usr: returnUSR, location: location)
}
}
private extension SourceKittenDictionary {
var swiftlintOffset: Int? {
return value["swiftlint.offset"] as? Int
}
var usr: String? {
return value["key.usr"] as? String
}
var annotatedDeclaration: String? {
return value["key.annotated_decl"] as? String
}
func aclAtOffset(_ offset: Int64) -> AccessControlLevel? {
if let nameOffset = nameOffset,
nameOffset == offset,
let acl = accessibility {
return acl
}
for child in substructure {
if let acl = child.aclAtOffset(offset) {
return acl
}
}
return nil
}
}
/// Only top-level functions and values can reliably be determined to be overexposed.
private let declarationKindsToLint: Set<SwiftDeclarationKind> = [ // TODO: Expand supported kinds
.functionFree,
.varGlobal,
.struct,
.enum,
.class
]
/// Skip syntax kinds that won't respond to cursor info requests.
private let syntaxKindsToSkip: Set<SyntaxKind> = [
.attributeBuiltin,
.attributeID,
.comment,
.commentMark,
.commentURL,
.buildconfigID,
.buildconfigKeyword,
.docComment,
.docCommentField,
.keyword,
.number,
.string,
.stringInterpolationAnchor
]

View File

@ -184,6 +184,7 @@
8F0856EB22DA8508001FF4D4 /* UnusedDeclarationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0856EA22DA8508001FF4D4 /* UnusedDeclarationRule.swift */; };
8F2CC1CB20A6A070006ED34F /* FileNameConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CA20A6A070006ED34F /* FileNameConfiguration.swift */; };
8F2CC1CD20A6A189006ED34F /* FileNameRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CC20A6A189006ED34F /* FileNameRuleTests.swift */; };
8F52160023BC08CD00309ADE /* OverexposedDeclarationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F5215FF23BC08CD00309ADE /* OverexposedDeclarationRule.swift */; };
8F6AA75B211905B8009BA28A /* LintableFilesVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6AA75A211905B8009BA28A /* LintableFilesVisitor.swift */; };
8F6AA75D21190830009BA28A /* CompilerArgumentsExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6AA75C21190830009BA28A /* CompilerArgumentsExtractor.swift */; };
8F715B83213B528B00427BD9 /* UnusedImportRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F715B82213B528B00427BD9 /* UnusedImportRule.swift */; };
@ -671,6 +672,7 @@
8F0856EA22DA8508001FF4D4 /* UnusedDeclarationRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedDeclarationRule.swift; sourceTree = "<group>"; };
8F2CC1CA20A6A070006ED34F /* FileNameConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileNameConfiguration.swift; sourceTree = "<group>"; };
8F2CC1CC20A6A189006ED34F /* FileNameRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileNameRuleTests.swift; sourceTree = "<group>"; };
8F5215FF23BC08CD00309ADE /* OverexposedDeclarationRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverexposedDeclarationRule.swift; sourceTree = "<group>"; };
8F6AA75A211905B8009BA28A /* LintableFilesVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LintableFilesVisitor.swift; sourceTree = "<group>"; };
8F6AA75C21190830009BA28A /* CompilerArgumentsExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompilerArgumentsExtractor.swift; sourceTree = "<group>"; };
8F715B82213B528B00427BD9 /* UnusedImportRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnusedImportRule.swift; sourceTree = "<group>"; };
@ -1161,6 +1163,7 @@
D40AD0891E032F9700F48C30 /* UnusedClosureParameterRule.swift */,
D4D7320C21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift */,
8F0856EA22DA8508001FF4D4 /* UnusedDeclarationRule.swift */,
8F5215FF23BC08CD00309ADE /* OverexposedDeclarationRule.swift */,
8F715B82213B528B00427BD9 /* UnusedImportRule.swift */,
D4BED5F72278AECC00D86BCE /* UnownedVariableCaptureRule.swift */,
D4B3409C21F16B110038C79A /* UnusedSetterValueRule.swift */,
@ -2026,6 +2029,7 @@
D4CFC5D2209EC95A00668488 /* FunctionDefaultParameterAtEndRule.swift in Sources */,
E88198541BEA945100333A11 /* CommaRule.swift in Sources */,
22A36FE123578CC10037B47D /* ExpiringTodoConfiguration.swift in Sources */,
8F52160023BC08CD00309ADE /* OverexposedDeclarationRule.swift in Sources */,
D4DA1DFE1E1A10DB0037413D /* NumberSeparatorConfiguration.swift in Sources */,
E88198601BEA98F000333A11 /* IdentifierNameRule.swift in Sources */,
E88DEA791B098D4400A66CB0 /* RuleParameter.swift in Sources */,

View File

@ -26,9 +26,18 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
onlyGenerateCoverageForSpecifiedTargets = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0E7B63119E9C64500EDBA4D"
BuildableName = "swiftlint.app"
BlueprintName = "swiftlint"
ReferencedContainer = "container:SwiftLint.xcodeproj">
</BuildableReference>
</MacroExpansion>
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
@ -52,24 +61,14 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0E7B63119E9C64500EDBA4D"
BuildableName = "swiftlint.app"
BlueprintName = "swiftlint"
ReferencedContainer = "container:SwiftLint.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
useCustomWorkingDirectory = "YES"
customWorkingDirectory = "~/Projects/Lyft-iOS"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "NO"
debugXPCServices = "NO"
@ -88,7 +87,11 @@
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "lint --no-cache"
argument = "analyze --compiler-log-path /Users/jsimard/Projects/Yams/xcodebuild.log"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "analyze --compile-commands /private/var/folders/8_/sm32hcd162v1dm0fw00gyrph0000gn/T/tmp.AFJsIr5v/sourcekit.deadcode.compile_commands.json"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
@ -104,8 +107,6 @@
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"

View File

@ -983,6 +983,12 @@ extension OperatorUsageWhitespaceRuleTests {
]
}
extension OverexposedDeclarationRuleTests {
static var allTests: [(String, (OverexposedDeclarationRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
]
}
extension OverriddenSuperCallRuleTests {
static var allTests: [(String, (OverriddenSuperCallRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
@ -1711,6 +1717,7 @@ XCTMain([
testCase(OpeningBraceRuleTests.allTests),
testCase(OperatorFunctionWhitespaceRuleTests.allTests),
testCase(OperatorUsageWhitespaceRuleTests.allTests),
testCase(OverexposedDeclarationRuleTests.allTests),
testCase(OverriddenSuperCallRuleTests.allTests),
testCase(OverrideInExtensionRuleTests.allTests),
testCase(PatternMatchingKeywordsRuleTests.allTests),

View File

@ -492,6 +492,12 @@ class OperatorUsageWhitespaceRuleTests: XCTestCase {
}
}
class OverexposedDeclarationRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(OverexposedDeclarationRule.description)
}
}
class OverriddenSuperCallRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(OverriddenSuperCallRule.description)