diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55ed53d222..05a3115b30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,11 @@
[Ornithologist Coder](https://github.com/ornithocoder)
[#1987](https://github.com/realm/SwiftLint/issues/1987)
+* Adds `prefixed_toplevel_constant` opt-in rule which encourages top-level
+ constants to be prefixed by `k`.
+ [Ornithologist Coder](https://github.com/ornithocoder)
+ [#1907](https://github.com/realm/SwiftLint/issues/1907)
+
##### Bug Fixes
* Fix false positives in `control_statement` rule when methods with keyword
diff --git a/Rules.md b/Rules.md
index a345a70e45..3cded3e549 100644
--- a/Rules.md
+++ b/Rules.md
@@ -75,6 +75,7 @@
* [Overridden methods call super](#overridden-methods-call-super)
* [Override in Extension](#override-in-extension)
* [Pattern Matching Keywords](#pattern-matching-keywords)
+* [Prefixed Top-Level Constant](#prefixed-top-level-constant)
* [Private Actions](#private-actions)
* [Private Outlets](#private-outlets)
* [Private over fileprivate](#private-over-fileprivate)
@@ -8960,6 +8961,85 @@ switch foo {
+## Prefixed Top-Level Constant
+
+Identifier | Enabled by default | Supports autocorrection | Kind
+--- | --- | --- | ---
+`prefixed_toplevel_constant` | Disabled | No | style
+
+Top-level constants should be prefixed by `k`.
+
+### Examples
+
+
+Non Triggering Examples
+
+```swift
+private let kFoo = 20.0
+```
+
+```swift
+public let kFoo = false
+```
+
+```swift
+internal let kFoo = "Foo"
+```
+
+```swift
+let kFoo = true
+```
+
+```swift
+struct Foo {
+ let bar = 20.0
+}
+```
+
+```swift
+private var foo = 20.0
+```
+
+```swift
+public var foo = false
+```
+
+```swift
+internal var foo = "Foo"
+```
+
+```swift
+var foo = true
+```
+
+
+
+Triggering Examples
+
+```swift
+private let ↓Foo = 20.0
+```
+
+```swift
+public let ↓Foo = false
+```
+
+```swift
+internal let ↓Foo = "Foo"
+```
+
+```swift
+let ↓Foo = true
+```
+
+```swift
+let ↓foo = 2, ↓bar = true
+```
+
+
+
+
+
## Private Actions
Identifier | Enabled by default | Supports autocorrection | Kind
diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift
index 97b8aa1b6e..47e9495c11 100644
--- a/Source/SwiftLintFramework/Models/MasterRuleList.swift
+++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift
@@ -83,6 +83,7 @@ public let masterRuleList = RuleList(rules: [
OverriddenSuperCallRule.self,
OverrideInExtensionRule.self,
PatternMatchingKeywordsRule.self,
+ PrefixedTopLevelConstantRule.self,
PrivateActionRule.self,
PrivateOutletRule.self,
PrivateOverFilePrivateRule.self,
diff --git a/Source/SwiftLintFramework/Rules/PrefixedTopLevelConstantRule.swift b/Source/SwiftLintFramework/Rules/PrefixedTopLevelConstantRule.swift
new file mode 100644
index 0000000000..11038e7c64
--- /dev/null
+++ b/Source/SwiftLintFramework/Rules/PrefixedTopLevelConstantRule.swift
@@ -0,0 +1,62 @@
+//
+// PrefixedConstantRule.swift
+// SwiftLint
+//
+// Created by Ornithologist Coder on 1/5/18.
+// Copyright © 2018 Realm. All rights reserved.
+//
+
+import Foundation
+import SourceKittenFramework
+
+public struct PrefixedTopLevelConstantRule: ASTRule, OptInRule, ConfigurationProviderRule {
+ public var configuration = SeverityConfiguration(.warning)
+
+ public init() {}
+
+ public static let description = RuleDescription(
+ identifier: "prefixed_toplevel_constant",
+ name: "Prefixed Top-Level Constant",
+ description: "Top-level constants should be prefixed by `k`.",
+ kind: .style,
+ nonTriggeringExamples: [
+ "private let kFoo = 20.0",
+ "public let kFoo = false",
+ "internal let kFoo = \"Foo\"",
+ "let kFoo = true",
+ "struct Foo {\n" +
+ " let bar = 20.0\n" +
+ "}",
+ "private var foo = 20.0",
+ "public var foo = false",
+ "internal var foo = \"Foo\"",
+ "var foo = true"
+ ],
+ triggeringExamples: [
+ "private let ↓Foo = 20.0",
+ "public let ↓Foo = false",
+ "internal let ↓Foo = \"Foo\"",
+ "let ↓Foo = true",
+ "let ↓foo = 2, ↓bar = true"
+ ]
+ )
+
+ public func validate(file: File,
+ kind: SwiftDeclarationKind,
+ dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
+ guard
+ kind == .varGlobal,
+ let offset = dictionary.offset,
+ let nameOffset = dictionary.nameOffset,
+ let name = dictionary.name,
+ let content = file.contents.bridge().substringWithByteRange(start: offset, length: nameOffset - offset),
+ content.hasPrefix("let") && !name.hasPrefix("k")
+ else {
+ return []
+ }
+
+ return [StyleViolation(ruleDescription: type(of: self).description,
+ severity: configuration.severity,
+ location: Location(file: file, byteOffset: nameOffset))]
+ }
+}
diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj
index 1d56303ca1..161613df16 100644
--- a/SwiftLint.xcodeproj/project.pbxproj
+++ b/SwiftLint.xcodeproj/project.pbxproj
@@ -83,6 +83,7 @@
629C60D91F43906700B4AF92 /* SingleTestClassRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629C60D81F43906700B4AF92 /* SingleTestClassRule.swift */; };
62A498561F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */; };
62A6E7931F3317E3003A0479 /* JoinedDefaultRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */; };
+ 62DADC481FFF0423002B6319 /* PrefixedTopLevelConstantRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DADC471FFF0423002B6319 /* PrefixedTopLevelConstantRule.swift */; };
62DEA1661FB21A9E00BCCCC6 /* PrivateActionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DEA1651FB21A9E00BCCCC6 /* PrivateActionRule.swift */; };
67932E2D1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */; };
67EB4DFA1E4CC111004E9ACD /* CyclomaticComplexityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */; };
@@ -430,6 +431,7 @@
62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitConfiguration.swift; sourceTree = ""; };
62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedDefaultRule.swift; sourceTree = ""; };
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRuleTests.swift; sourceTree = ""; };
+ 62DADC471FFF0423002B6319 /* PrefixedTopLevelConstantRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixedTopLevelConstantRule.swift; sourceTree = ""; };
62DEA1651FB21A9E00BCCCC6 /* PrivateActionRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateActionRule.swift; sourceTree = ""; };
62E54FED1F93AD57005B367B /* QuickDiscouragedFocusedTestRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDiscouragedFocusedTestRule.swift; sourceTree = ""; };
65454F451B14D73800319A6C /* ControlStatementRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatementRule.swift; sourceTree = ""; };
@@ -1092,6 +1094,7 @@
78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */,
D40FE89C1F867BFF006433E2 /* OverrideInExtensionRule.swift */,
D403A4A21F4DB5510020CA02 /* PatternMatchingKeywordsRule.swift */,
+ 62DADC471FFF0423002B6319 /* PrefixedTopLevelConstantRule.swift */,
62DEA1651FB21A9E00BCCCC6 /* PrivateActionRule.swift */,
094385021D5D4F78009168CF /* PrivateOutletRule.swift */,
1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */,
@@ -1557,6 +1560,7 @@
57ED827B1CF656E3002B3513 /* JUnitReporter.swift in Sources */,
D43B04691E072291004016AF /* ColonConfiguration.swift in Sources */,
E82367E01ED3BD1E0040A88E /* Configuration+Cache.swift in Sources */,
+ 62DADC481FFF0423002B6319 /* PrefixedTopLevelConstantRule.swift in Sources */,
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index 2c928abac0..abd1ebf63b 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -438,6 +438,7 @@ extension RulesTests {
("testOperatorUsageWhitespace", testOperatorUsageWhitespace),
("testOverrideInExtension", testOverrideInExtension),
("testPatternMatchingKeywords", testPatternMatchingKeywords),
+ ("testPrefixedTopLevelConstant", testPrefixedTopLevelConstant),
("testPrivateAction", testPrivateAction),
("testPrivateOutlet", testPrivateOutlet),
("testPrivateUnitTest", testPrivateUnitTest),
diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift
index 0e7eeef313..e8e7bec673 100644
--- a/Tests/SwiftLintFrameworkTests/RulesTests.swift
+++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift
@@ -259,6 +259,10 @@ class RulesTests: XCTestCase {
verifyRule(PatternMatchingKeywordsRule.description)
}
+ func testPrefixedTopLevelConstant() {
+ verifyRule(PrefixedTopLevelConstantRule.description)
+ }
+
func testPrivateAction() {
verifyRule(PrivateActionRule.description)
}