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

Comment style #5148

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cffff34
(feat) added comment style rule
mredig Jul 24, 2023
65a4749
(refactor) groups up comments as they are grouped in code
mredig Jul 24, 2023
d45334f
(refactor) conform to convention for configuration error thrown
mredig Jul 25, 2023
d185cff
(refactor) specify generated examples as passing
mredig Jul 25, 2023
ababff5
(refactor) create some triggering examples
mredig Jul 25, 2023
cc3279e
(refactor) set up configuration
mredig Jul 25, 2023
bd62e86
(fix) add triggering examples to description
mredig Jul 25, 2023
09b83aa
(refactor) include severity in config
mredig Jul 25, 2023
3c52fae
(refactor) add logic for comment_style rule
mredig Jul 25, 2023
c95624f
(refactor) multibyte offset was causing issues, so made a manual vari…
mredig Jul 26, 2023
d4e8dea
(fix) comment style apply fix
mredig Jul 26, 2023
a15c19f
(refactor) configured generated examples with correct comment style g…
mredig Jul 26, 2023
6a634af
(fix) disable multibyte and comment tests in examples file
mredig Jul 26, 2023
f4106be
(refactor) normalized initialization route
mredig Jul 26, 2023
766de1c
(refactor) skip disabled lines
mredig Jul 26, 2023
453550a
(refactor) allow focus in example info
mredig Jul 31, 2023
c3900b6
(fix) when // swiftlint:disable is used in a multiline required style…
mredig Jul 31, 2023
a742f49
(refactor) swiftlint conformance
mredig Jul 31, 2023
7cc69dd
(nit) spelling
mredig Jul 31, 2023
b9785cb
(nit) updated changelog
mredig Jul 31, 2023
428371a
(refactor) set .swiftlint.yml to enforce comment_style as `mixed`
mredig Jul 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ file_name:
- GeneratedTests.swift
- SwiftSyntax+SwiftLint.swift
- TestHelpers.swift
comment_style:
comment_style: mixed

balanced_xctest_lifecycle: &unit_test_configuration
test_parent_classes:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
[keith](https://github.com/keith)
[5139](https://github.com/realm/SwiftLint/pull/5139)

* Add `comment_style` rules to allow specifying whether a project should have a
specific comment style, between multiline and single line comments.
[Michael Redig](https://github.com/mredig)
[PR 5148](https://github.com/realm/SwiftLint/pull/5148)

#### Bug Fixes

* Fix false positive in `control_statement` rule that triggered on conditions
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public let builtInRules: [Rule.Type] = [
CommaInheritanceRule.self,
CommaRule.self,
CommentSpacingRule.self,
CommentStyleRule.self,
CompilerProtocolInitRule.self,
ComputedAccessorsOrderRule.self,
ConditionalReturnsOnNewlineRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import SwiftLintCore

struct CommentStyleConfiguration: RuleConfiguration, Equatable {
typealias Parent = CommentStyleRule

/// Set this value to enforce a comment style for the entire project. If unset, each file will be evaluated
/// individually and enforce consistency with whatever style appears first within the file.
@ConfigurationElement(key: "comment_style")
private(set) var commentStyle: Style?
/// The number of single line comments allowed before requiring a multiline comment.
/// Only valid when `comment_style` is set to `mixed`
@ConfigurationElement(key: "line_comment_threshold")
private(set) var lineCommentThreshold: Int?
@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)

mutating func apply(configuration: Any) throws {
guard
let configurationDict = configuration as? [String: Any]
else { throw Issue.unknownConfiguration(ruleID: Parent.identifier) }

if let commentStyleString: String = configurationDict[key: $commentStyle] {
let style = Style(rawValue: commentStyleString)
style != nil ? () : print("'\(commentStyleString)' invalid for comment style. No style enforce.")
self.commentStyle = style
}

if
commentStyle == .mixed,
let lineCommentThreshold: Int = configurationDict[key: $lineCommentThreshold] {
self.lineCommentThreshold = lineCommentThreshold
}

if let severityString: String = configurationDict[key: $severityConfiguration] {
try severityConfiguration.apply(configuration: severityString)
}
}
}

extension CommentStyleConfiguration {
enum Style: String, AcceptableByConfigurationElement {
case multiline
case singleline
case mixed

func asOption() -> SwiftLintCore.OptionType { .string(rawValue) }
}
}

extension Dictionary where Key == String, Value == Any {
subscript<T>(key key: String) -> T? {
self[key] as? T
}
}
221 changes: 221 additions & 0 deletions Source/SwiftLintBuiltInRules/Rules/Style/CommentStyleRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import Foundation
import SwiftIDEUtils
import SwiftSyntax

struct CommentStyleRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
typealias ConfigurationType = CommentStyleConfiguration

var configuration = ConfigurationType()

static let description = RuleDescription(
identifier: "comment_style",
name: "Comment Style",
description: """
Allows for options to enforce styling of comments. Should all comments be multiline comments? Should they be single
line? Or should there be a threshold, where if x number of single line comments are in a row, should they be
multiline?
""",
kind: .style,
minSwiftVersion: .current,
nonTriggeringExamples: CommentStyleRuleExamples.generateNonTriggeringExamples(),
triggeringExamples: CommentStyleRuleExamples.generateTriggeringExamples())

func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(configuration: configuration, file: file)
}
}

extension CommentStyleRule {
class Visitor: ViolationsSyntaxVisitor {
typealias Parent = CommentStyleRule // swiftlint:disable:this nesting

private let allCommentGroupings: [CommentGrouping]
let file: SwiftLintFile

let configuration: Parent.ConfigurationType

typealias Style = Parent.ConfigurationType.Style // swiftlint:disable:this nesting
private var commentStyle: Style?

enum CommentGrouping { // swiftlint:disable:this nesting
case singleline([SyntaxClassifiedRange])
case multiline([SyntaxClassifiedRange])
case singlelineDoc([SyntaxClassifiedRange])
case multilineDoc([SyntaxClassifiedRange])
}

init(configuration: Parent.ConfigurationType, file: SwiftLintFile) {
self.configuration = configuration
self.file = file

var rangeGroupings: [CommentGrouping] = []
var commentsAccumulator: [SyntaxClassifiedRange] = []

func appendCommentsAccumulator() {
guard commentsAccumulator.isEmpty == false else { return }

guard
let kind = commentsAccumulator.first?.kind,
commentsAccumulator.allSatisfy({ $0.kind == kind })
else { queuedFatalError("Accumulator acquired mixed comment kind...") }

switch kind {
case .lineComment:
rangeGroupings.append(.singleline(commentsAccumulator))
case .blockComment:
rangeGroupings.append(.multiline(commentsAccumulator))
case .docLineComment:
rangeGroupings.append(.singlelineDoc(commentsAccumulator))
case .docBlockComment:
rangeGroupings.append(.multilineDoc(commentsAccumulator))
default:
queuedFatalError("non comment in comment accumulator")
}
commentsAccumulator.removeAll()
}

let commandLines = file.commands.reduce(into: Set<Int>()) { commandLines, command in
commandLines.insert(command.line)
}

let fileData = Data(file.contents.utf8)
for classificationRange in file.syntaxClassifications {
let location = file.locationConverter.location(for: AbsolutePosition(utf8Offset: classificationRange.offset))
guard commandLines.contains(location.line) == false else { continue }

switch classificationRange.kind {
case _ where classificationRange.kind.isComment == true:
if commentsAccumulator.last?.kind != classificationRange.kind {
appendCommentsAccumulator()
}
commentsAccumulator.append(classificationRange)
case .none:
if
let text = Self.convertToString(from: classificationRange, withOriginalStringData: fileData),
text.countOccurrences(of: "\n") > 1 {
appendCommentsAccumulator()
}
default:
appendCommentsAccumulator()
}
}
appendCommentsAccumulator()

self.allCommentGroupings = rangeGroupings
self.commentStyle = configuration.commentStyle
super.init(viewMode: .sourceAccurate)
}

override func visitPost(_ node: SourceFileSyntax) {
for grouping in allCommentGroupings {
switch grouping {
case .singleline(let commentGroup):
visitPostComment(commentGroup)
case .multiline(let commentGroup):
visitPostBlockComment(commentGroup)
case .singlelineDoc(let commentGroup):
visitPostDocComment(commentGroup)
case .multilineDoc(let commentGroup):
visitPostBlockDockComment(commentGroup)
}
}
}

func visitPostComment(_ commentRangeGroup: [SyntaxClassifiedRange]) {
guard let firstCommentInGroup = commentRangeGroup.first else { return }
if case .fail(let reason, let severity) = validateCommentStyle(.singleline) {
appendViolation(
at: AbsolutePosition(utf8Offset: firstCommentInGroup.offset),
reason: reason,
severity: severity)
return
}

if case .fail(let reason, let severity) = validateSinglelineCommentLineLength(commentRangeGroup.count) {
appendViolation(
at: AbsolutePosition(utf8Offset: firstCommentInGroup.offset),
reason: reason,
severity: severity)
return
}
}

func visitPostBlockComment(_ commentRangeGroup: [SyntaxClassifiedRange]) {
guard let firstCommentInGroup = commentRangeGroup.first else { return }
if case .fail(let reason, let severity) = validateCommentStyle(.multiline) {
appendViolation(
at: AbsolutePosition(utf8Offset: firstCommentInGroup.offset),
reason: reason,
severity: severity)
return
}
}

func visitPostDocComment(_ commentRangeGroup: [SyntaxClassifiedRange]) {}

func visitPostBlockDockComment(_ commentRangeGroup: [SyntaxClassifiedRange]) {}

private func validateCommentStyle(_ style: Style) -> Validation {
let commentStyle = self.commentStyle ?? style
self.commentStyle = commentStyle

switch commentStyle {
case .mixed:
return .pass
default:
guard style == commentStyle else {
return .fail(
reason: "\(style.rawValue.capitalized) comments not allowed",
severity: configuration.severityConfiguration.severity)
}
return .pass
}
}

private func validateSinglelineCommentLineLength(_ lineLength: Int) -> Validation {
guard
commentStyle == .mixed,
let threshold = configuration.lineCommentThreshold
else { return .pass }

if lineLength < threshold {
return .pass
} else {
return .fail(
reason: "Block comments required for comments spanning \(threshold) or more lines",
severity: configuration.severityConfiguration.severity)
}
}

private static func convertToString(
from range: SyntaxClassifiedRange,
withOriginalStringData originalStringData: Data) -> String? {
let content = originalStringData[range.range.dataRange]
return String(data: content, encoding: .utf8)
}

@discardableResult
private func appendViolation(
at position: AbsolutePosition,
reason: String,
severity: ViolationSeverity) -> ReasonedRuleViolation {
let violation = ReasonedRuleViolation(
position: position,
reason: reason,
severity: severity)
violations.append(violation)
return violation
}

enum Validation { // swiftlint:disable:this nesting
case pass
case fail(reason: String, severity: ViolationSeverity)
}
}
}

private extension ByteSourceRange {
var dataRange: Range<Int> {
offset..<endOffset
}
}