diff --git a/.swiftlint.yml b/.swiftlint.yml index 68017b0c5..a5a511533 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -48,6 +48,7 @@ opt_in_rules: - overridden_super_call - override_in_extension - pattern_matching_keywords + - period_spacing - prefer_self_type_over_type_of_self - private_action - private_outlet diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b3a7462..11aa86940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,11 @@ `single_test_class` and `empty_xctest_method` rules. [Martin Redington](https://github.com/mildm8nnered) [#4200](https://github.com/realm/SwiftLint/issues/4200) + + * Add `period_spacing` opt-in rule that checks periods are not followed + by 2 or more spaces in comments. + [Julioacarrettoni](https://github.com/Julioacarrettoni) + [#4624](https://github.com/realm/SwiftLint/pull/4624) * Show warnings in the console for Analyzer rules that are listed in the `opt_in_rules` configuration section. diff --git a/Source/SwiftLintFramework/Extensions/SyntaxClassification+isComment.swift b/Source/SwiftLintFramework/Extensions/SyntaxClassification+isComment.swift new file mode 100644 index 000000000..f3e7bf5ae --- /dev/null +++ b/Source/SwiftLintFramework/Extensions/SyntaxClassification+isComment.swift @@ -0,0 +1,15 @@ +import IDEUtils + +extension SyntaxClassification { + // True if it is any kind of comment. + var isComment: Bool { + switch self { + case .lineComment, .docLineComment, .blockComment, .docBlockComment: + return true + case .none, .keyword, .identifier, .typeIdentifier, .operatorIdentifier, .dollarIdentifier, .integerLiteral, + .floatingLiteral, .stringLiteral, .stringInterpolationAnchor, .poundDirectiveKeyword, .buildConfigId, + .attribute, .objectLiteral, .editorPlaceholder: + return false + } + } +} diff --git a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift index 025f6ebcb..cd725956f 100644 --- a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift +++ b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift @@ -137,6 +137,7 @@ public let primaryRuleList = RuleList(rules: [ OverriddenSuperCallRule.self, OverrideInExtensionRule.self, PatternMatchingKeywordsRule.self, + PeriodSpacingRule.self, PreferNimbleRule.self, PreferSelfInStaticReferencesRule.self, PreferSelfTypeOverTypeOfSelfRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/CommentSpacingRule.swift b/Source/SwiftLintFramework/Rules/Lint/CommentSpacingRule.swift index 8688132b6..426c62889 100644 --- a/Source/SwiftLintFramework/Rules/Lint/CommentSpacingRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/CommentSpacingRule.swift @@ -122,15 +122,9 @@ struct CommentSpacingRule: SourceKitFreeRule, ConfigurationProviderRule, Substit func violationRanges(in file: SwiftLintFile) -> [NSRange] { // Find all comment tokens in the file and regex search them for violations file.syntaxClassifications - .compactMap { (classifiedRange: SyntaxClassifiedRange) -> [NSRange]? in - switch classifiedRange.kind { - case .blockComment, .docBlockComment, .lineComment, .docLineComment: - break - default: - return nil - } - - let range = classifiedRange.range.toSourceKittenByteRange() + .filter(\.kind.isComment) + .map { $0.range.toSourceKittenByteRange() } + .compactMap { (range: ByteRange) -> [NSRange]? in return file.stringView .substringWithByteRange(range) .map(StringView.init) diff --git a/Source/SwiftLintFramework/Rules/Lint/PeriodSpacingRule.swift b/Source/SwiftLintFramework/Rules/Lint/PeriodSpacingRule.swift new file mode 100644 index 000000000..12a4c6a34 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/PeriodSpacingRule.swift @@ -0,0 +1,94 @@ +import Foundation +import IDEUtils +import SourceKittenFramework + +struct PeriodSpacingRule: SourceKitFreeRule, ConfigurationProviderRule, OptInRule, SubstitutionCorrectableRule { + var configuration = SeverityConfiguration(.warning) + + init() {} + + static let description = RuleDescription( + identifier: "period_spacing", + name: "Period Spacing", + description: "Periods should not be followed by more than one space.", + kind: .style, + nonTriggeringExamples: [ + Example("let pi = 3.2"), + Example("let pi = Double.pi"), + Example("let pi = Double. pi"), + Example("let pi = Double. pi"), + Example("// A. Single."), + Example("/// - code: Identifier of the error. Integer."), + Example(""" + // value: Multiline. + // Comment. + """), + Example(""" + /** + Sentence ended in period. + + - Sentence 2 new line characters after. + **/ + """) + ], + triggeringExamples: [ + Example("/* Only god knows why. ↓ This symbol does nothing. */", testWrappingInComment: false), + Example("// Only god knows why. ↓ This symbol does nothing.", testWrappingInComment: false), + Example("// Single. Double. ↓ End.", testWrappingInComment: false), + Example("// Single. Double. ↓ Triple. ↓ End.", testWrappingInComment: false), + Example("// Triple. ↓ Quad. ↓ End.", testWrappingInComment: false), + Example("/// - code: Identifier of the error. ↓ Integer.", testWrappingInComment: false) + ], + corrections: [ + Example("/* Why. ↓ Symbol does nothing. */"): Example("/* Why. Symbol does nothing. */"), + Example("// Why. ↓ Symbol does nothing."): Example("// Why. Symbol does nothing."), + Example("// Single. Double. ↓ End."): Example("// Single. Double. End."), + Example("// Single. Double. ↓ Triple. ↓ End."): Example("// Single. Double. Triple. End."), + Example("// Triple. ↓ Quad. ↓ End."): Example("// Triple. Quad. End."), + Example("/// - code: Identifier. ↓ Integer."): Example("/// - code: Identifier. Integer.") + ] + ) + + func violationRanges(in file: SwiftLintFile) -> [NSRange] { + // Find all comment tokens in the file and regex search them for violations + file.syntaxClassifications + .filter(\.kind.isComment) + .map { $0.range.toSourceKittenByteRange() } + .compactMap { (range: ByteRange) -> [NSRange]? in + return file.stringView + .substringWithByteRange(range) + .map(StringView.init) + .map { commentBody in + // Look for a period followed by two or more whitespaces but not new line or carriage returns + return regex(#"\.[^\S\r\n]{2,}"#) + .matches(in: commentBody) + .compactMap { result in + // Set the location to start from the second whitespace till the last one. + return file.stringView.byteRangeToNSRange( + ByteRange( + // Safe to mix NSRange offsets with byte offsets here because the + // regex can't contain multi-byte characters + location: ByteCount(range.lowerBound.value + result.range.lowerBound + 2), + length: ByteCount(result.range.length.advanced(by: -2)) + ) + ) + } + } + } + .flatMap { $0 } + } + + func validate(file: SwiftLintFile) -> [StyleViolation] { + return violationRanges(in: file).map { range in + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, characterOffset: range.location) + ) + } + } + + func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { + return (violationRange, "") + } +} diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 09a2b0a75..20bfea51e 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -811,6 +811,12 @@ class PatternMatchingKeywordsRuleGeneratedTests: XCTestCase { } } +class PeriodSpacingRuleGeneratedTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(PeriodSpacingRule.description) + } +} + class PreferNimbleRuleGeneratedTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(PreferNimbleRule.description)