Skip to content

Commit c252626

Browse files
authored
Set up SwiftLint as a standalone build plugin (#23996)
* Wrap SwiftLint call from aggretate target in script This way, we'll be able to add additional checks without messing up the project file. * Do not run build-time SwiftLint when building in CI * Only run SwiftLint build phase in Debug builds * Set up SwiftLint as a SwiftPM plugin in dedicated BuildTools pkg * Set SDK when running SwiftLint to avoid conflicts * Use SwiftLint versin 0.58.2 The previous version, 0.54.0, is not available as a plugin. And of all the version available via the plugin, this is the only one that actually works. * Fix `opening_braces` violation using `swiftlint --fix` * Merge remote and local SwiftLint configs–Plugin has trouble with remote
1 parent 48e8ad5 commit c252626

File tree

13 files changed

+194
-32
lines changed

13 files changed

+194
-32
lines changed

Diff for: .swiftlint.yml

+100-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,118 @@
1-
swiftlint_version: 0.54.0
2-
3-
parent_config: https://raw.githubusercontent.com/Automattic/swiftlint-config/0f8ab6388bd8d15a04391825ab125f80cfb90704/.swiftlint.yml
4-
remote_timeout: 10.0
1+
swiftlint_version: 0.58.2
52

63
excluded:
4+
- BuildTools/.build
5+
- DerivedData
76
- Modules/.build
7+
- Scripts
8+
- WordPress/DerivedData
9+
- fastlane
10+
- vendor
11+
12+
# Rules – Opt-in only, so we can progressively introduce new ones
13+
#
14+
only_rules:
15+
# Colons should be next to the identifier when specifying a type.
16+
- colon
17+
18+
# There should be no space before and one after any comma.
19+
- comma
20+
21+
# if,for,while,do statements shouldn't wrap their conditionals in parentheses.
22+
- control_statement
23+
24+
# Allow custom rules. See the end of the config for our custom rules
25+
- custom_rules
826

9-
opt_in_rules:
1027
- discarded_notification_center_observer
28+
1129
- duplicate_imports
30+
31+
# Arguments can be omitted when matching enums with associated types if they
32+
# are not used.
33+
- empty_enum_arguments
34+
35+
# Prefer `() -> ` over `Void -> `.
36+
- empty_parameters
37+
38+
# MARK comment should be in valid format.
39+
- mark
40+
41+
# Opening braces should be preceded by a single space and on the same line as
42+
# the declaration.
43+
- opening_brace
44+
1245
- overridden_super_call
46+
1347
- shorthand_optional_binding
48+
49+
# Files should have a single trailing newline.
50+
- trailing_newline
51+
52+
# Lines should not have trailing semicolons.
53+
- trailing_semicolon
54+
55+
# Lines should not have trailing whitespace.
56+
- trailing_whitespace
57+
1458
- vertical_whitespace
59+
1560
- weak_delegate
1661

17-
overridden_super_call:
62+
# Rules configuration
63+
#
64+
control_statement:
1865
severity: error
1966

2067
discarded_notification_center_observer:
2168
severity: error
2269

70+
overridden_super_call:
71+
severity: error
72+
73+
trailing_whitespace:
74+
ignores_empty_lines: false
75+
ignores_comments: false
76+
2377
weak_delegate:
2478
severity: error
79+
80+
# Custom rules
81+
#
82+
custom_rules:
83+
natural_content_alignment:
84+
name: "Natural Content Alignment"
85+
regex: '\.contentHorizontalAlignment(\s*)=(\s*)(\.left|\.right)'
86+
message: "Forcing content alignment left or right can affect the Right-to-Left layout. Use naturalContentHorizontalAlignment instead."
87+
severity: warning
88+
89+
natural_text_alignment:
90+
name: "Natural Text Alignment"
91+
regex: '\.textAlignment(\s*)=(\s*).left'
92+
message: "Forcing text alignment to left can affect the Right-to-Left layout. Consider setting it to `natural`"
93+
severity: warning
94+
95+
inverse_text_alignment:
96+
name: "Inverse Text Alignment"
97+
regex: '\.textAlignment(\s*)=(\s*).right'
98+
message: "When forcing text alignment to the right, be sure to handle the Right-to-Left layout case properly, and then silence this warning with this line `// swiftlint:disable:next inverse_text_alignment`"
99+
severity: warning
100+
101+
localization_comment:
102+
name: "Localization Comment"
103+
regex: 'NSLocalizedString([^,]+,\s+comment:\s*"")'
104+
message: "Localized strings should include a description giving context for how the string is used."
105+
severity: warning
106+
107+
string_interpolation_in_localized_string:
108+
name: "String Interpolation in Localized String"
109+
regex: 'NSLocalizedString\("[^"]*\\\(\S*\)'
110+
message: "Localized strings must not use interpolated variables. Instead, use `String(format:`"
111+
severity: error
112+
113+
swiftui_localization:
114+
name: "SwiftUI Localization"
115+
regex: 'LocalizedStringKey'
116+
message: "Using `LocalizedStringKey` is incompatible with our tooling and doesn't allow you to provide a hint/context comment for translators either. Please use `NSLocalizedString` instead, even with SwiftUI code."
117+
severity: error
118+
excluded: '.*Widgets/.*'

Diff for: BuildTools/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.build

Diff for: BuildTools/Empty.swift

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Here only to satisfy SwiftPM requirement.
2+
// See https://github.com/nicklockwood/SwiftFormat#1-create-a-buildtools-folder-and-packageswift

Diff for: BuildTools/Package.resolved

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"originHash" : "cd02791f9079102404056ed65d89c5c6bb997332bbfd1b03550dfdd51e57551e",
3+
"pins" : [
4+
{
5+
"identity" : "swiftlintplugins",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
8+
"state" : {
9+
"revision" : "7a3d77f3dd9f91d5cea138e52c20cfceabf352de",
10+
"version" : "0.58.2"
11+
}
12+
}
13+
],
14+
"version" : 3
15+
}

Diff for: BuildTools/Package.swift

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// swift-tools-version:6.0
2+
import Foundation
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "BuildTools",
7+
platforms: [.macOS(.v10_13)],
8+
dependencies: [
9+
.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: loadSwiftLintVersion()),
10+
],
11+
targets: [.target(name: "BuildTools", path: "")]
12+
)
13+
14+
func loadSwiftLintVersion() -> Version {
15+
let swiftLintConfigURL = URL(fileURLWithPath: #filePath)
16+
.deletingLastPathComponent()
17+
.appendingPathComponent("..")
18+
.appendingPathComponent(".swiftlint.yml")
19+
20+
guard let yamlString = try? String(contentsOf: swiftLintConfigURL) else {
21+
fatalError("Failed to read SwiftLint config file at \(swiftLintConfigURL).")
22+
}
23+
24+
guard let versionLine = yamlString.components(separatedBy: .newlines)
25+
.first(where: { $0.contains("swiftlint_version") }) else {
26+
fatalError("SwiftLint version not found in YAML file.")
27+
}
28+
29+
// Assumes the format `swiftlint_version: <version>`
30+
guard let version = Version(versionLine.components(separatedBy: ":")
31+
.last?
32+
.trimmingCharacters(in: .whitespaces) ?? "") else {
33+
fatalError("Failed to extract SwiftLint version.")
34+
}
35+
36+
return version
37+
}

Diff for: Rakefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ end
209209

210210
desc 'Checks the source for style errors'
211211
task :lint do
212-
puts 'No linter configured at the moment.'
212+
sh 'pushd BuildTools; export SDKROOT=$(xcrun --sdk macosx --show-sdk-path); swift package plugin --allow-writing-to-directory .. --allow-writing-to-package-directory swiftlint --working-directory .. --quiet; popd'
213213
end
214214

215215
namespace :git do

Diff for: Scripts/BuildPhases/SwiftLint.sh

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash -e
2+
3+
# Do not run in CI environments.
4+
# Our CI has its own static linter.
5+
# As of 2025/01, this should save some 20-40s per build.
6+
if [ -n "${CI+x}" ]; then
7+
echo 'CI environment detected. Skipping SwiftLint build phase in favor of dedicated CI process.'
8+
exit 0
9+
fi
10+
11+
# Only run on debug builds.
12+
# For some reason, running when trying to archive, via CLI, results in a compilation failure.
13+
#
14+
# fatal error: module 'WordPressSharedObjC' in AST file '/path/to/DerivedData/ModuleCache.noindex/EQUUY9BHSJ5N/WordPressSharedObjC-5G93B85NZ09I.pcm'
15+
# (imported by AST file '/Users/gio/Developer/a8c/wpios/DerivedData/WordPress/Build/Intermediates.noindex/ArchiveIntermediates/WordPress Alpha/PrecompiledHeaders/WordPress-Bridging-Header-swift_1L0UBHDEION2G-clang_EQUUY9BHSJ5N.pch')
16+
# is not defined in any loaded module map file;
17+
# maybe you need to load '/Users/gio/Developer/a8c/wpios/DerivedData/WordPress/Build/Intermediates.noindex/ArchiveIntermediates/WordPress Alpha/IntermediateBuildFilesPath/GeneratedModuleMaps-iphoneos/WordPressSharedObjC.modulemap'?
18+
if [ "${CONFIGURATION}" != "Debug" ]; then
19+
echo 'Running in a build configuration other than Debug. Skipping SwiftLint in production builds.'
20+
exit 0
21+
fi
22+
23+
rake lint

Diff for: WordPress/Classes/Extensions/NSAttributedString+Helpers.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ public extension NSAttributedString {
3333
if
3434
displayAnimatedGifs,
3535
let animatedImage = image as? AnimatedImage,
36-
animatedImage.gifData != nil
37-
{
36+
animatedImage.gifData != nil {
3837
imageAttachment.contents = animatedImage.gifData
3938
imageAttachment.fileType = gifType
4039
imageAttachment.bounds = CGRect(origin: CGPoint.zero, size: animatedImage.targetSize ?? image.size)

Diff for: WordPress/Classes/Extensions/URL+AVKit.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ extension URL {
1414
let imageSource = CGImageSourceCreateWithURL(self as NSURL, nil),
1515
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary?) as NSDictionary?,
1616
let pixelWidth = imageProperties[kCGImagePropertyPixelWidth as NSString] as? Int,
17-
let pixelHeight = imageProperties[kCGImagePropertyPixelHeight as NSString] as? Int
18-
{
17+
let pixelHeight = imageProperties[kCGImagePropertyPixelHeight as NSString] as? Int {
1918
return CGSize(width: pixelWidth, height: pixelHeight)
2019
}
2120
}

Diff for: WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ class AbstractPostListViewController: UIViewController,
1212
NSFetchedResultsControllerDelegate,
1313
UITableViewDelegate,
1414
UITableViewDataSource,
15-
NetworkAwareUI // This protocol is not in an extension so that subclasses can override noConnectionMessage()
16-
{
15+
NetworkAwareUI { // This protocol is not in an extension so that subclasses can override noConnectionMessage()
1716
typealias SyncPostResult = (posts: [AbstractPost], hasMore: Bool)
1817

1918
private static let httpErrorCodeForbidden = 403

Diff for: WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift

+4-8
Original file line numberDiff line numberDiff line change
@@ -247,30 +247,26 @@ extension PostTagPickerViewController: UITextViewDelegate {
247247
range.length == 1 && text == "", // Deleting last character
248248
range.location > 0, // Not at the beginning
249249
range.location + range.length == original.length, // At the end
250-
original.substring(with: NSRange(location: range.location - 1, length: 1)) == "," // Previous is a comma
251-
{
250+
original.substring(with: NSRange(location: range.location - 1, length: 1)) == "," { // Previous is a comma
252251
// Delete the comma as well
253252
textView.text = original.substring(to: range.location - 1) + original.substring(from: range.location + range.length)
254253
textView.selectedRange = NSRange(location: range.location - 1, length: 0)
255254
textViewDidChange(textView)
256255
return false
257256
} else if range.length == 0, // Inserting
258257
text == ",", // a comma
259-
range.location == original.length // at the end
260-
{
258+
range.location == original.length { // at the end
261259
// Append a space
262260
textView.text = original.replacingCharacters(in: range, with: ", ")
263261
textViewDidChange(textView)
264262
return false
265263
} else if text == "\n", // return
266264
range.location == original.length, // at the end
267-
!partialTag.isEmpty // with some (partial) tag typed
268-
{
265+
!partialTag.isEmpty { // with some (partial) tag typed
269266
textView.text = original.replacingCharacters(in: range, with: ", ")
270267
textViewDidChange(textView)
271268
return false
272-
} else if text == "\n" // return anywhere else
273-
{
269+
} else if text == "\n" { // return anywhere else
274270
return false
275271
}
276272
return true

Diff for: WordPress/WordPress.xcodeproj/project.pbxproj

+4-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
isa = PBXAggregateTarget;
4545
buildConfigurationList = FFA8E22F1F94E3DE0002170F /* Build configuration list for PBXAggregateTarget "SwiftLint" */;
4646
buildPhases = (
47-
FFA8E2301F94E3EF0002170F /* ShellScript */,
47+
FFA8E2301F94E3EF0002170F /* SwiftLint */,
4848
);
4949
dependencies = (
5050
);
@@ -9056,19 +9056,20 @@
90569056
shellPath = /bin/sh;
90579057
shellScript = "\"$SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.sh\"\n";
90589058
};
9059-
FFA8E2301F94E3EF0002170F /* ShellScript */ = {
9059+
FFA8E2301F94E3EF0002170F /* SwiftLint */ = {
90609060
isa = PBXShellScriptBuildPhase;
90619061
alwaysOutOfDate = 1;
90629062
buildActionMask = 2147483647;
90639063
files = (
90649064
);
90659065
inputPaths = (
90669066
);
9067+
name = SwiftLint;
90679068
outputPaths = (
90689069
);
90699070
runOnlyForDeploymentPostprocessing = 0;
90709071
shellPath = /bin/sh;
9071-
shellScript = "rake lint\n";
9072+
shellScript = "sh \"${SRCROOT}/../Scripts/BuildPhases/SwiftLint.sh\"\n";
90729073
showEnvVarsInLog = 0;
90739074
};
90749075
/* End PBXShellScriptBuildPhase section */

Diff for: WordPress/WordPressShareExtension/ShareTagsPickerViewController.swift

+4-8
Original file line numberDiff line numberDiff line change
@@ -265,30 +265,26 @@ extension ShareTagsPickerViewController: UITextViewDelegate {
265265
range.length == 1 && text == "", // Deleting last character
266266
range.location > 0, // Not at the beginning
267267
range.location + range.length == original.length, // At the end
268-
original.substring(with: NSRange(location: range.location - 1, length: 1)) == "," // Previous is a comma
269-
{
268+
original.substring(with: NSRange(location: range.location - 1, length: 1)) == "," { // Previous is a comma
270269
// Delete the comma as well
271270
textView.text = original.substring(to: range.location - 1) + original.substring(from: range.location + range.length)
272271
textView.selectedRange = NSRange(location: range.location - 1, length: 0)
273272
textViewDidChange(textView)
274273
return false
275274
} else if range.length == 0, // Inserting
276275
text == ",", // a comma
277-
range.location == original.length // at the end
278-
{
276+
range.location == original.length { // at the end
279277
// Append a space
280278
textView.text = original.replacingCharacters(in: range, with: ", ")
281279
textViewDidChange(textView)
282280
return false
283281
} else if text == "\n", // return
284282
range.location == original.length, // at the end
285-
!partialTag.isEmpty // with some (partial) tag typed
286-
{
283+
!partialTag.isEmpty { // with some (partial) tag typed
287284
textView.text = original.replacingCharacters(in: range, with: ", ")
288285
textViewDidChange(textView)
289286
return false
290-
} else if text == "\n" // return anywhere else
291-
{
287+
} else if text == "\n" { // return anywhere else
292288
return false
293289
}
294290
return true

0 commit comments

Comments
 (0)