Compare commits
2 Commits
main
...
overexpose
Author | SHA1 | Date |
---|---|---|
![]() |
01dfce3a5c | |
![]() |
395f23db13 |
|
@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"))] : []),
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -112,6 +112,7 @@ public let masterRuleList = RuleList(rules: [
|
|||
OpeningBraceRule.self,
|
||||
OperatorFunctionWhitespaceRule.self,
|
||||
OperatorUsageWhitespaceRule.self,
|
||||
OverexposedDeclarationRule.self,
|
||||
OverriddenSuperCallRule.self,
|
||||
OverrideInExtensionRule.self,
|
||||
PatternMatchingKeywordsRule.self,
|
||||
|
|
|
@ -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
|
||||
]
|
|
@ -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 */,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -492,6 +492,12 @@ class OperatorUsageWhitespaceRuleTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
class OverexposedDeclarationRuleTests: XCTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule(OverexposedDeclarationRule.description)
|
||||
}
|
||||
}
|
||||
|
||||
class OverriddenSuperCallRuleTests: XCTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule(OverriddenSuperCallRule.description)
|
||||
|
|
Loading…
Reference in New Issue