storyboardlint 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +53 -1
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/storyboardlint.rb +135 -54
- data/storyboardlint.gemspec +3 -3
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb6bd687ab2eef94fe9d9aaf3fe66d5599c05886
|
|
4
|
+
data.tar.gz: 979f598428de5417814402939b1e456e04997c8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d0978e1b82ac952ee5ea8e7f3ea3286e769a3b9291e0d06f16dc48dd55ce519b6c9736bc41518d9ec6153e105ed85840f0ef0a3f3e2020ae9e4df23e6781a24
|
|
7
|
+
data.tar.gz: f3cbc4e6c6dfc117cbc7c4a87e4e0cbf74d02cb617bb6312ca2b821a30c4bc1d77a93aa32f9872bb621b2341f9c67206f20ea8cf8c57ea49efc91e620092e4e3
|
data/README.md
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
StoryboardLint
|
|
2
2
|
==============
|
|
3
3
|
|
|
4
|
-
A lint tool for UIStoryboard to find wrong classes and wrong storyboard/segue/reuse identifiers
|
|
4
|
+
A lint tool for UIStoryboard to find wrong classes and wrong storyboard/segue/reuse identifiers.
|
|
5
|
+
|
|
6
|
+
Background
|
|
7
|
+
----------
|
|
8
|
+
In iOS development, UIStoryboards are a pain to use [for many reasons](http://stackoverflow.com/questions/9404471/when-to-use-storyboard-and-when-to-use-xibs/19457257#19457257).
|
|
9
|
+
Two big reasons are:
|
|
10
|
+
- You can only use string literals as identifiers for view controllers and segues in your Storyboards. You need those same string literals again in your source code when referencing anything in your Storyboard: So you have your IDs in two or more places and no way to know if the IDs you use in your code actually exist in your Storyboard.
|
|
11
|
+
- You have no way of knowing whether your Storyboard references classes in your source code that don't exist (anymore).
|
|
12
|
+
|
|
13
|
+
Fear Not!
|
|
14
|
+
---------
|
|
15
|
+
With StoryboardLint, you can finally make sure that your code and your Storyboards are in sync (at least to a certain degree). StoryboardLint does the following things:
|
|
16
|
+
|
|
17
|
+
- Makes sure your `UITableViewCell` and `UICollectionViewCell` reuse identifiers are named according to StoryboardLint's naming convention.
|
|
18
|
+
- Makes sure your Storyboard and Segue identifiers are named according to StoryboardLint's naming convention.
|
|
19
|
+
- Makes sure that all custom classes that are references from your Storyboard actually exist is your code.
|
|
20
|
+
- Makes sure that all string literals in your code that reference reuse identifiers, Storyboard identifiers and Segue identifiers (according to the naming convention) actually exist in your Storyboard(s).
|
|
21
|
+
- Produces output that is parsable by Xcode so you can easily plug StoryboardLint into your build process:
|
|
22
|
+
|
|
23
|
+
**Warnings right in your source code:**
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
**Warnings about bugs in your Storyboard:**
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
Naming Convention
|
|
32
|
+
-----------------
|
|
33
|
+
In order for StoryboardLint to find the string literals in your code that contain reuse/Storyboard/Segue identifiers, you need to prefix them as follows:
|
|
34
|
+
|
|
35
|
+
- Reuse Identifiers start with `ruid_`. Example: `ruid_ProductTableViewCell`.
|
|
36
|
+
- Storyboard Identifiers start with `sb_`. Example: `sb_ProductsViewController`.
|
|
37
|
+
- Segue Identifiers start with `seg_`. Example: `seg_ProductDetailsSegue`.
|
|
38
|
+
|
|
39
|
+
This should not be too much work and it will be worth it. Ideally you already have string constants for these things in your code, so that you only have to edit two places: your string constant in code and the respective value in your Storyboard.
|
|
40
|
+
|
|
41
|
+
Installation
|
|
42
|
+
------------
|
|
43
|
+
Just install the Ruby Gem:
|
|
44
|
+
|
|
45
|
+
sudo gem install storyboardlint
|
|
46
|
+
|
|
47
|
+
In your Xcode Project, simply add a "Run Script" build phase in order to run StoryboardLint every time you build:
|
|
48
|
+
|
|
49
|
+
1. Go to your project and select your project's target in the "Targets" section.
|
|
50
|
+
2. Go to "Build Phases"
|
|
51
|
+
3. Select from the Menu "Editor -> Add Build Phase -> Add Run Script Build Phase"
|
|
52
|
+
4. Paste this command into the Run Script section: `storyboardlint "$SRCROOT"`
|
|
53
|
+
5. That's it!
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
data/Rakefile
CHANGED
|
@@ -16,7 +16,7 @@ Jeweler::Tasks.new do |gem|
|
|
|
16
16
|
gem.homepage = "https://github.com/jfahrenkrug/StoryboardLint"
|
|
17
17
|
gem.license = "MIT"
|
|
18
18
|
gem.summary = %Q{A lint tool for UIStoryboards to find wrong classes and wrong storyboard/segue/reuse identifiers}
|
|
19
|
-
gem.description = %Q{It's a pain to to keep identifier strings in your UIStoryboard and
|
|
19
|
+
gem.description = %Q{It's a pain to to keep identifier strings in your UIStoryboard and in your source code in sync. This tool helps you to do just that.}
|
|
20
20
|
gem.email = "johannes@springenwerk.com"
|
|
21
21
|
gem.authors = ["Johannes Fahrenkrug"]
|
|
22
22
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.1
|
data/lib/storyboardlint.rb
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
# Copyright (c) 2014 Johannes Fahrenkrug
|
|
3
3
|
|
|
4
4
|
require 'nokogiri'
|
|
5
|
+
require 'ostruct'
|
|
6
|
+
require 'optparse'
|
|
5
7
|
|
|
6
8
|
module StoryboardLint
|
|
7
|
-
SEGUE_ID_PREFIX = "seg_"
|
|
8
|
-
STORYBOARD_ID_PREFIX = "sb_"
|
|
9
|
-
REUSE_ID_PREFIX = "ruid_"
|
|
10
|
-
|
|
11
9
|
class StoryboardScanner
|
|
12
10
|
def initialize(src_root)
|
|
13
11
|
@src_root = src_root
|
|
@@ -66,13 +64,9 @@ module StoryboardLint
|
|
|
66
64
|
end
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
class SourceScanner
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
STORYBOARD_ID_REGEX = /@"(#{STORYBOARD_ID_PREFIX}(?:\\"|[^"])+)"/
|
|
73
|
-
REUSE_ID_REGEX = /@"(#{REUSE_ID_PREFIX}(?:\\"|[^"])+)"/
|
|
74
|
-
|
|
75
|
-
def initialize(src_root)
|
|
67
|
+
class SourceScanner
|
|
68
|
+
def initialize(src_root, matcher)
|
|
69
|
+
@matcher = matcher
|
|
76
70
|
@src_root = src_root
|
|
77
71
|
@scan_performed = false
|
|
78
72
|
end
|
|
@@ -94,22 +88,22 @@ module StoryboardLint
|
|
|
94
88
|
source_files.each do |source_file|
|
|
95
89
|
File.readlines(source_file).each_with_index do |line, idx|
|
|
96
90
|
# class names
|
|
97
|
-
line.scan(
|
|
91
|
+
line.scan(@matcher.class_regex).each do |match|
|
|
98
92
|
@class_names << {:file => source_file, :line => idx + 1, :class_name => match[0]}
|
|
99
93
|
end
|
|
100
94
|
|
|
101
95
|
# segue ids
|
|
102
|
-
line.scan(
|
|
96
|
+
line.scan(@matcher.segue_id_regex_source).each do |match|
|
|
103
97
|
@segue_ids << {:file => source_file, :line => idx + 1, :id => match[0]}
|
|
104
98
|
end
|
|
105
99
|
|
|
106
100
|
# storyboard ids
|
|
107
|
-
line.scan(
|
|
101
|
+
line.scan(@matcher.storyboard_id_regex_source).each do |match|
|
|
108
102
|
@storyboard_ids << {:file => source_file, :line => idx + 1, :id => match[0]}
|
|
109
103
|
end
|
|
110
104
|
|
|
111
105
|
# reuse ids
|
|
112
|
-
line.scan(
|
|
106
|
+
line.scan(@matcher.reuse_id_regex_source).each do |match|
|
|
113
107
|
@reuse_ids << {:file => source_file, :line => idx + 1, :id => match[0]}
|
|
114
108
|
end
|
|
115
109
|
end
|
|
@@ -140,8 +134,67 @@ module StoryboardLint
|
|
|
140
134
|
end
|
|
141
135
|
end
|
|
142
136
|
|
|
137
|
+
class Matcher
|
|
138
|
+
DEFAULT_SEGUE_ID_PREFIX = "seg_"
|
|
139
|
+
DEFAULT_STORYBOARD_ID_PREFIX = "sb_"
|
|
140
|
+
DEFAULT_REUSE_ID_PREFIX = "ruid_"
|
|
141
|
+
|
|
142
|
+
def initialize(options)
|
|
143
|
+
options ||= OpenStruct.new
|
|
144
|
+
|
|
145
|
+
@storyboard_id_regex_source = create_source_regex(DEFAULT_STORYBOARD_ID_PREFIX, options.storyboard_prefix, options.storyboard_suffix)
|
|
146
|
+
@storyboard_id_regex_sb = create_storyboard_regex(DEFAULT_STORYBOARD_ID_PREFIX, options.storyboard_prefix, options.storyboard_suffix)
|
|
147
|
+
|
|
148
|
+
@segue_id_regex_source = create_source_regex(DEFAULT_SEGUE_ID_PREFIX, options.segue_prefix, options.segue_suffix)
|
|
149
|
+
@segue_id_regex_sb = create_storyboard_regex(DEFAULT_SEGUE_ID_PREFIX, options.segue_prefix, options.segue_suffix)
|
|
150
|
+
|
|
151
|
+
@reuse_id_regex_source = create_source_regex(DEFAULT_REUSE_ID_PREFIX, options.reuse_prefix, options.reuse_suffix)
|
|
152
|
+
@reuse_id_regex_sb = create_storyboard_regex(DEFAULT_REUSE_ID_PREFIX, options.reuse_prefix, options.reuse_suffix)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def create_source_regex(default_prefix, prefix, suffix)
|
|
156
|
+
inner_regex_part = %{(?:\\"|[^"])+}
|
|
157
|
+
if prefix.to_s.empty? and suffix.to_s.empty?
|
|
158
|
+
return /@"(#{default_prefix}#{inner_regex_part})"/
|
|
159
|
+
else
|
|
160
|
+
return /@"(#{prefix}#{inner_regex_part}#{suffix})"/
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def create_storyboard_regex(default_prefix, prefix, suffix)
|
|
165
|
+
inner_regex_part = %{(?:\\"|[^"])+}
|
|
166
|
+
if prefix.to_s.empty? and suffix.to_s.empty?
|
|
167
|
+
sb = /^#{default_prefix}/
|
|
168
|
+
else
|
|
169
|
+
if !prefix.to_s.empty?
|
|
170
|
+
if !suffix.to_s.empty?
|
|
171
|
+
sb = /^#{prefix}[\w\s]*#{suffix}$/
|
|
172
|
+
else !prefix.to_s.empty?
|
|
173
|
+
sb = /^#{prefix}/
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
sb = /#{suffix}$/
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
sb
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def class_regex
|
|
184
|
+
/@interface\s+([a-zA-Z_]+\w*)/
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
[:storyboard, :segue, :reuse].each do |name|
|
|
188
|
+
[:sb, :source].each do |kind|
|
|
189
|
+
method_name = "#{name}_id_regex_#{kind}"
|
|
190
|
+
define_method(method_name) { instance_variable_get("@#{method_name}") }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
143
195
|
class Linter
|
|
144
|
-
def initialize(sb_scanner, source_scanner)
|
|
196
|
+
def initialize(sb_scanner, source_scanner, matcher)
|
|
197
|
+
@matcher = matcher
|
|
145
198
|
@sb_scanner = sb_scanner
|
|
146
199
|
@source_scanner = source_scanner
|
|
147
200
|
end
|
|
@@ -153,21 +206,13 @@ module StoryboardLint
|
|
|
153
206
|
end
|
|
154
207
|
|
|
155
208
|
def check_naming
|
|
156
|
-
@sb_scanner.segue_ids.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if sb_id[:id] !~ /$#{STORYBOARD_ID_PREFIX}/
|
|
164
|
-
puts "warning: Storyboard ID '#{sb_id[:id]}' used in #{File.basename(sb_id[:file])} does not start with '#{STORYBOARD_ID_PREFIX}' prefix."
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
@sb_scanner.reuse_ids.each do |ru_id|
|
|
169
|
-
if ru_id[:id] !~ /$#{REUSE_ID_PREFIX}/
|
|
170
|
-
puts "warning: Reuse ID '#{ru_id[:id]}' used in #{File.basename(ru_id[:file])} does not start with '#{REUSE_ID_PREFIX}' prefix."
|
|
209
|
+
[{:items => @sb_scanner.segue_ids, :regex => @matcher.segue_id_regex_sb, :name => 'Segue ID'},
|
|
210
|
+
{:items => @sb_scanner.storyboard_ids, :regex => @matcher.storyboard_id_regex_sb, :name => 'Storyboard ID'},
|
|
211
|
+
{:items => @sb_scanner.reuse_ids, :regex => @matcher.reuse_id_regex_sb, :name => 'Reuse ID'}].each do |data|
|
|
212
|
+
data[:items].each do |item|
|
|
213
|
+
if item[:id] !~ data[:regex]
|
|
214
|
+
puts "warning: #{data[:name]} '#{item[:id]}' used in #{File.basename(item[:file])} does not match '#{data[:regex]}."
|
|
215
|
+
end
|
|
171
216
|
end
|
|
172
217
|
end
|
|
173
218
|
end
|
|
@@ -181,39 +226,75 @@ module StoryboardLint
|
|
|
181
226
|
end
|
|
182
227
|
|
|
183
228
|
def check_ids
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if !@sb_scanner.storyboard_ids.map {|sb_sb_id| sb_sb_id[:id]}.include?(sb_id[:id])
|
|
192
|
-
puts "#{sb_id[:file]}:#{sb_id[:line]}: warning: Storyboard ID '#{sb_id[:id]}' could not be found in any Storyboard."
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
@source_scanner.reuse_ids.each do |ru_id|
|
|
197
|
-
if !@sb_scanner.reuse_ids.map {|sb_ru_id| sb_ru_id[:id]}.include?(ru_id[:id])
|
|
198
|
-
puts "#{ru_id[:file]}:#{ru_id[:line]}: warning: Reuse ID '#{ru_id[:id]}' could not be found in any Storyboard."
|
|
229
|
+
[{:method_name => :segue_ids, :name => 'Segue ID'},
|
|
230
|
+
{:method_name => :storyboard_ids, :name => 'Storyboard ID'},
|
|
231
|
+
{:method_name => :reuse_ids, :name => 'Reuse ID'}].each do |data|
|
|
232
|
+
@source_scanner.send(data[:method_name]).each do |source_item|
|
|
233
|
+
if !@sb_scanner.send(data[:method_name]).map {|sb_item| sb_item[:id]}.include?(source_item[:id])
|
|
234
|
+
puts "#{source_item[:file]}:#{source_item[:line]}: warning: #{data[:name]} '#{source_item[:id]}' could not be found in any Storyboard."
|
|
235
|
+
end
|
|
199
236
|
end
|
|
200
237
|
end
|
|
201
238
|
end
|
|
202
239
|
|
|
203
240
|
def self.run!(*args)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
241
|
+
options = OpenStruct.new
|
|
242
|
+
|
|
243
|
+
opt_parser = OptionParser.new do |opts|
|
|
244
|
+
opts.banner = "Usage: storyboardlint <target directory> [options]"
|
|
245
|
+
opts.separator ""
|
|
246
|
+
opts.separator "Options"
|
|
247
|
+
|
|
248
|
+
opts.on("--storyboard-prefix [PREFIX]", "Storyboard IDs have to begin with PREFIX.") do |prefix|
|
|
249
|
+
options.storyboard_prefix = prefix
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
opts.on("--storyboard-suffix [SUFFIX]", "Storyboard IDs have to end with SUFFIX") do |suffix|
|
|
253
|
+
options.storyboard_suffix = suffix
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
opts.on("--segue-prefix [PREFIX]", "Segue IDs have to begin with PREFIX") do |prefix|
|
|
257
|
+
options.segue_prefix = prefix
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
opts.on("--segue-suffix [SUFFIX]", "Segue IDs have to end with SUFFIX") do |suffix|
|
|
261
|
+
options.segue_suffix = suffix
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
opts.on("--reuse-prefix [PREFIX]", "Reuse IDs have to begin with PREFIX") do |prefix|
|
|
265
|
+
options.reuse_prefix = prefix
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
opts.on("--reuse-suffix [SUFFIX]", "Reuse IDs have to end with SUFFIX") do |suffix|
|
|
269
|
+
options.reuse_suffix = suffix
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# No argument, shows at tail. This will print an options summary.
|
|
273
|
+
# Try it and see!
|
|
274
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
|
275
|
+
puts opts
|
|
276
|
+
exit
|
|
277
|
+
end
|
|
207
278
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
279
|
+
# Another typical switch to print the version.
|
|
280
|
+
opts.on_tail("--version", "Show version") do
|
|
281
|
+
puts "StoryboardLint v0.1.1"
|
|
282
|
+
exit
|
|
283
|
+
end
|
|
211
284
|
end
|
|
285
|
+
|
|
286
|
+
if ARGV.length < 1
|
|
287
|
+
puts opt_parser
|
|
288
|
+
exit 0
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
opt_parser.parse(args)
|
|
212
292
|
|
|
293
|
+
matcher = StoryboardLint::Matcher.new(options)
|
|
213
294
|
sb_scanner = StoryboardLint::StoryboardScanner.new(ARGV[0])
|
|
214
|
-
source_scanner = StoryboardLint::SourceScanner.new(ARGV[0])
|
|
295
|
+
source_scanner = StoryboardLint::SourceScanner.new(ARGV[0], matcher)
|
|
215
296
|
|
|
216
|
-
linter = StoryboardLint::Linter.new(sb_scanner, source_scanner)
|
|
297
|
+
linter = StoryboardLint::Linter.new(sb_scanner, source_scanner, matcher)
|
|
217
298
|
linter.lint
|
|
218
299
|
|
|
219
300
|
return 0
|
data/storyboardlint.gemspec
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
Gem::Specification.new do |s|
|
|
7
7
|
s.name = "storyboardlint"
|
|
8
|
-
s.version = "0.1.
|
|
8
|
+
s.version = "0.1.1"
|
|
9
9
|
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
11
|
s.authors = ["Johannes Fahrenkrug"]
|
|
12
|
-
s.date = "2014-02-
|
|
13
|
-
s.description = "It's a pain to to keep identifier strings in your UIStoryboard and
|
|
12
|
+
s.date = "2014-02-20"
|
|
13
|
+
s.description = "It's a pain to to keep identifier strings in your UIStoryboard and in your source code in sync. This tool helps you to do just that."
|
|
14
14
|
s.email = "johannes@springenwerk.com"
|
|
15
15
|
s.executables = ["storyboardlint"]
|
|
16
16
|
s.extra_rdoc_files = [
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: storyboardlint
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johannes Fahrenkrug
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2014-02-
|
|
11
|
+
date: 2014-02-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -52,7 +52,7 @@ dependencies:
|
|
|
52
52
|
- - ~>
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: 1.8.0
|
|
55
|
-
description: It's a pain to to keep identifier strings in your UIStoryboard and
|
|
55
|
+
description: It's a pain to to keep identifier strings in your UIStoryboard and in
|
|
56
56
|
your source code in sync. This tool helps you to do just that.
|
|
57
57
|
email: johannes@springenwerk.com
|
|
58
58
|
executables:
|