ducalis 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c78c0b5ad28cbd122b68f755abbbb3f1aa052700
4
- data.tar.gz: 63c7074a2d5f41fe414939710ba902e4bcb116ea
3
+ metadata.gz: c6fa5fd04439b5d750ef52ab27f19c4fe233dc6c
4
+ data.tar.gz: a700248bc70906a88c759217a76c60873df249c8
5
5
  SHA512:
6
- metadata.gz: a091ccfd11f2fbbb626014065a2d95f9e0a79b930763d1ec9c3919b4c695ea4cd8cbb1ae301e43be979a895ce72801668a75cc49be4aa63f5d1743e3a5969ec0
7
- data.tar.gz: 5e080337b542b2c56b8207ce8dff250be08e6a7cd20606553422d2ee39709d1f84d9cc9c9f6908d235c5c74b528bd3839050753b7874c857595fdee64ec3eee2
6
+ metadata.gz: e297975d9b71e10ea881e99eebe64af49a756dbf1e5d6167da25431893408697794ee6125f61b60da2981d4dbe0e91a58377b92750a7433b08872e1f00ec64f1
7
+ data.tar.gz: 5c29c471688d345b4433cf60fe7b488a39ce5499d847a3474edccadab255f4a115dd3bee52fcabd76a50b32f93f8227e7d74312edca74fcdacf148d4bacee33d
data/.codeclimate.yml ADDED
@@ -0,0 +1,6 @@
1
+ engines:
2
+ rubocop:
3
+ enabled: true
4
+ channel: rubocop-0-50
5
+ config:
6
+ file: .rubocop.yml
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.4
3
3
  UseCache: false
4
+ Exclude:
5
+ - 'client/vendor/bundle/**/*'
6
+ - 'vendor/bundle/**/*'
4
7
 
5
8
  Metrics/BlockLength:
6
9
  Exclude:
data/DOCUMENTATION.md CHANGED
@@ -1,7 +1,8 @@
1
1
  ## Ducalis::CallbacksActiverecord
2
2
 
3
3
  Please, avoid using of callbacks for models. It's better to keep models small ("dumb") and instead use "builder" classes/services: to construct new objects. You can read more [here](https://medium.com/planet-arkency/a61fd75ab2d3).
4
- - rejects ActiveRecord classes which contains callbacks
4
+
5
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises on ActiveRecord classes which contains callbacks
5
6
  ```ruby
6
7
 
7
8
  class A < ActiveRecord::Base
@@ -9,18 +10,116 @@ class A < ActiveRecord::Base
9
10
  end
10
11
 
11
12
  ```
12
- - ignores non-ActiveRecord classes which contains callbacks
13
+
14
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores non-ActiveRecord classes which contains callbacks
13
15
  ```ruby
14
16
 
15
17
  class A < SomeBasicClass
16
18
  before_create :generate_code
17
19
  end
18
20
 
21
+ ```
22
+ ## Ducalis::CaseMapping
23
+
24
+ Try to avoid `case when` statements. You can replace it with a sequence of
25
+ `if... elsif... elsif... else`. For cases where you need to choose from a
26
+ large number of possibilities, you can create a dictionary mapping case values
27
+ to functions to call by `call`. It's nice to have prefix for the method
28
+ names, i.e.: `visit_`.
29
+
30
+ <details>
31
+ Usually `case when` statements are using for the next reasons:
32
+
33
+ I. Mapping between different values.
34
+ ("A" => 1, "B" => 2, ...)
35
+
36
+ This case is all about data representing. If you do not need to execute any code
37
+ it's better to use data structure which represents it. This way you are
38
+ separating concepts: code returns corresponding value and you have config-like
39
+ data structure which describes your data.
40
+
41
+ ```ruby
42
+ %w[A B ...].index("A") + 1
43
+ # or
44
+ { "A" => 1, "B" => 2 }.fetch("A")
45
+ ```
46
+
47
+ II. Code execution depending of parameter or type:
48
+
49
+ - a. (:attack => attack, :defend => defend)
50
+ - b. (Feet => value * 0.348, Meters => `value`)
51
+
52
+ In this case code violates OOP and S[O]LID principle. Code shouldn't know about
53
+ object type and classes should be open for extension, but closed for
54
+ modification (but you can't do it with case-statements).
55
+ This is a signal that you have some problems with architecture.
56
+
57
+ a.
58
+ ```ruby
59
+ attack: -> { execute_attack }, defend: -> { execute_defend }
60
+ # or
61
+ call(:"execute_#{action}")
62
+ ```
63
+
64
+ b.
65
+ ```ruby
66
+ class Meters; def to_metters; value; end
67
+ class Feet; def to_metters; value * 0.348; end
68
+ ```
69
+
70
+ III. Code execution depending on some statement.
71
+ (`a > 0` => 1, `a == 0` => 0, `a < 0` => -1)
72
+
73
+ This case is combination of I and II -- high code complexity and unit-tests
74
+ complexity. There are variants how to solve it:
75
+
76
+ a. Rewrite to simple if statement
77
+
78
+ ```ruby
79
+ return 0 if a == 0
80
+ a > 0 ? 1 : -1
81
+ ```
82
+
83
+ b. Move statements to lambdas:
84
+
85
+ ```ruby
86
+ ->(a) { a > 0 } => 1,
87
+ ->(a) { a == 0 } => 0,
88
+ ->(a) { a < 0 } => -1
89
+ ```
90
+
91
+ This way decreases code complexity by delegating it to lambdas and makes it easy
92
+ to unit-testing because it's easy to test pure lambdas.
93
+
94
+ Such approach is named
95
+ [table-driven design](<https://www.d.umn.edu/~gshute/softeng/table-driven.html>)
96
+ . Table-driven methods are schemes that allow you to look up information in a
97
+ table rather than using logic statements (i.e. case, if). In simple cases,
98
+ it's quicker and easier to use logic statements, but as the logic chain becomes
99
+ more complex, table-driven code is simpler than complicated logic, easier to
100
+ modify and more efficient.
101
+ </details>
102
+
103
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises on case statements
104
+ ```ruby
105
+
106
+ case grade
107
+ when "A"
108
+ puts "Well done!"
109
+ when "B"
110
+ puts "Try harder!"
111
+ when "C"
112
+ puts "You need help!!!"
113
+ else
114
+ puts "You just making it up!"
115
+ end
116
+
19
117
  ```
20
118
  ## Ducalis::ControllersExcept
21
119
 
22
120
  Prefer to use `:only` over `:except` in controllers because it's more explicit and will be easier to maintain for new developers.
23
- - raises for `before_filters` with `except` method as array
121
+
122
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for `before_filters` with `except` method as array
24
123
  ```ruby
25
124
 
26
125
  class MyController < ApplicationController
@@ -32,7 +131,8 @@ class MyController < ApplicationController
32
131
  end
33
132
 
34
133
  ```
35
- - raises for filters with many actions and only one `except` method
134
+
135
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for filters with many actions and only one `except` method
36
136
  ```ruby
37
137
 
38
138
  class MyController < ApplicationController
@@ -45,7 +145,8 @@ class MyController < ApplicationController
45
145
  end
46
146
 
47
147
  ```
48
- - ignores `before_filters` without arguments
148
+
149
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores `before_filters` without arguments
49
150
  ```ruby
50
151
 
51
152
  class MyController < ApplicationController
@@ -59,30 +160,36 @@ end
59
160
  ## Ducalis::KeywordDefaults
60
161
 
61
162
  Prefer to use keyword arguments for defaults. It increases readability and reduces ambiguities.
62
- - rejects if method definition contains default values
163
+
164
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if method definition contains default values
63
165
  ```ruby
64
166
  def some_method(a, b, c = 3); end
65
167
  ```
66
- - rejects if class method definition contains default values
168
+
169
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if class method definition contains default values
67
170
  ```ruby
68
171
  def self.some_method(a, b, c = 3); end
69
172
  ```
70
- - works if method definition contains default values through keywords
173
+
174
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores if method definition contains default values through keywords
71
175
  ```ruby
72
176
  def some_method(a, b, c: 3); end
73
177
  ```
74
- - works for methods without arguments
178
+
179
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores for methods without arguments
75
180
  ```ruby
76
181
  def some_method; end
77
182
  ```
78
- - works for class methods without arguments
183
+
184
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores for class methods without arguments
79
185
  ```ruby
80
186
  def self.some_method; end
81
187
  ```
82
188
  ## Ducalis::ModuleLikeClass
83
189
 
84
190
  Seems like it will be better to define initialize and pass %<args>s there instead of each method.
85
- - raise if class doesn't contain constructor but accept the same args
191
+
192
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if class doesn't contain constructor but accept the same args
86
193
  ```ruby
87
194
 
88
195
  class MyClass
@@ -107,7 +214,8 @@ class MyClass
107
214
  end
108
215
 
109
216
  ```
110
- - raise for class with only one public method with args
217
+
218
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for class with only one public method with args
111
219
  ```ruby
112
220
 
113
221
  class MyClass
@@ -123,7 +231,8 @@ class MyClass
123
231
  end
124
232
 
125
233
  ```
126
- - ignores classes with custom includes
234
+
235
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores classes with custom includes
127
236
  ```ruby
128
237
 
129
238
  class MyClass
@@ -135,7 +244,8 @@ class MyClass
135
244
  end
136
245
 
137
246
  ```
138
- - ignores classes with inheritance
247
+
248
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores classes with inheritance
139
249
  ```ruby
140
250
 
141
251
  class MyClass < AnotherClass
@@ -151,7 +261,8 @@ class MyClass < AnotherClass
151
261
  end
152
262
 
153
263
  ```
154
- - ignores classes with one method and initializer
264
+
265
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores classes with one method and initializer
155
266
  ```ruby
156
267
 
157
268
  class MyClass
@@ -169,7 +280,8 @@ end
169
280
 
170
281
  It's better to pass already preprocessed params hash to services. Or you can use
171
282
  `arcane` gem
172
- - raise if user pass `params` as argument from controller
283
+
284
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if user pass `params` as argument from controller
173
285
  ```ruby
174
286
 
175
287
  class MyController < ApplicationController
@@ -179,7 +291,8 @@ class MyController < ApplicationController
179
291
  end
180
292
 
181
293
  ```
182
- - raise if user pass `params` as any argument from controller
294
+
295
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if user pass `params` as any argument from controller
183
296
  ```ruby
184
297
 
185
298
  class MyController < ApplicationController
@@ -189,7 +302,8 @@ class MyController < ApplicationController
189
302
  end
190
303
 
191
304
  ```
192
- - raise if user pass `params` as keyword argument from controller
305
+
306
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if user pass `params` as keyword argument from controller
193
307
  ```ruby
194
308
 
195
309
  class MyController < ApplicationController
@@ -199,7 +313,8 @@ class MyController < ApplicationController
199
313
  end
200
314
 
201
315
  ```
202
- - ignores passing only one `params` field
316
+
317
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores passing only one `params` field
203
318
  ```ruby
204
319
 
205
320
  class MyController < ApplicationController
@@ -209,7 +324,8 @@ class MyController < ApplicationController
209
324
  end
210
325
 
211
326
  ```
212
- - ignores passing processed `params`
327
+
328
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores passing processed `params`
213
329
  ```ruby
214
330
 
215
331
  class MyController < ApplicationController
@@ -219,7 +335,8 @@ class MyController < ApplicationController
219
335
  end
220
336
 
221
337
  ```
222
- - ignores passing `params` from `arcane` gem
338
+
339
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores passing `params` from `arcane` gem
223
340
  ```ruby
224
341
 
225
342
  class MyController < ApplicationController
@@ -228,11 +345,117 @@ class MyController < ApplicationController
228
345
  end
229
346
  end
230
347
 
348
+ ```
349
+ ## Ducalis::PossibleTap
350
+
351
+ Consider of using `.tap`, default ruby [method](<https://apidock.com/ruby/Object/tap>) which allows to replace intermediate variables with block, by this you are limiting scope pollution and make scope more clear. [Related article](<http://seejohncode.com/2012/01/02/ruby-tap-that/>).
352
+
353
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for methods with scope variable return
354
+ ```ruby
355
+
356
+ def load_group
357
+ group = channel.groups.find(params[:group_id])
358
+ authorize group, :edit?
359
+ group
360
+ end
361
+
362
+ ```
363
+
364
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for methods with instance variable changes and return
365
+ ```ruby
366
+
367
+ def load_group
368
+ @group = Group.find(params[:id])
369
+ authorize @group
370
+ @group
371
+ end
372
+
373
+ ```
374
+
375
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for methods with instance variable `||=` assign and return
376
+ ```ruby
377
+
378
+ def define_roles
379
+ return [] unless employee
380
+
381
+ @roles ||= []
382
+ @roles << "primary" if employee.primary?
383
+ @roles << "contract" if employee.contract?
384
+ @roles
385
+ end
386
+
387
+ ```
388
+
389
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for methods which return call on scope variable
390
+ ```ruby
391
+
392
+ def load_group
393
+ elections = @elections.group_by(&:code)
394
+ result = elections.map do |code, elections|
395
+ { code => statistic }
396
+ end
397
+ result << total_spend(@elections)
398
+ result.inject(:merge)
399
+ end
400
+
401
+ ```
402
+
403
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for methods which return instance variable but have scope vars
404
+ ```ruby
405
+
406
+ def generate_file(file_name)
407
+ @file = Tempfile.new([file_name, ".pdf"])
408
+ signed_pdf = some_new_stuff
409
+ @file.write(signed_pdf.to_pdf)
410
+ @file.close
411
+ @file
412
+ end
413
+
414
+ ```
415
+
416
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores empty methods
417
+ ```ruby
418
+
419
+ def edit
420
+ end
421
+
422
+ ```
423
+
424
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores methods which body is just call
425
+ ```ruby
426
+
427
+ def total_cost(cost_field)
428
+ Service.cost_sum(cost_field)
429
+ end
430
+
431
+ ```
432
+
433
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores methods which return some statement
434
+ ```ruby
435
+
436
+ def stop_terminated_employee
437
+ if current_user && current_user.terminated?
438
+ sign_out current_user
439
+ redirect_to new_user_session_path
440
+ end
441
+ end
442
+
443
+
444
+ ```
445
+
446
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores methods which simply returns instance var without changes
447
+ ```ruby
448
+
449
+ def employee
450
+ @employee
451
+ end
452
+
231
453
  ```
232
454
  ## Ducalis::PrivateInstanceAssign
233
455
 
234
456
  Please, don't assign instance variables in controller's private methods. It's make hard to understand what variables are available in views.
235
- - raises for assigning instance variables in controllers private methods
457
+
458
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for assigning instance variables in controllers private methods
236
459
  ```ruby
237
460
 
238
461
  class MyController < ApplicationController
@@ -244,7 +467,8 @@ class MyController < ApplicationController
244
467
  end
245
468
 
246
469
  ```
247
- - raises for memoization variables in controllers private methods
470
+
471
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for memoization variables in controllers private methods
248
472
  ```ruby
249
473
 
250
474
  class MyController < ApplicationController
@@ -256,7 +480,8 @@ class MyController < ApplicationController
256
480
  end
257
481
 
258
482
  ```
259
- - ignores memoization variables in controllers private methods with _
483
+
484
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores memoization variables in controllers private methods with _
260
485
  ```ruby
261
486
 
262
487
  class MyController < ApplicationController
@@ -268,7 +493,8 @@ class MyController < ApplicationController
268
493
  end
269
494
 
270
495
  ```
271
- - ignores assigning instance variables in controllers public methods
496
+
497
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores assigning instance variables in controllers public methods
272
498
  ```ruby
273
499
 
274
500
  class MyController < ApplicationController
@@ -295,30 +521,50 @@ current_group.employees.find(params[:id])
295
521
  # better then
296
522
  Employee.find(params[:id])
297
523
  ```
298
- - raise if somewhere AR search was called on not protected scope
524
+
525
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if somewhere AR search was called on not protected scope
299
526
  ```ruby
300
527
  Group.find(8)
301
528
  ```
302
- - raise if AR search was called even for chain of calls
529
+
530
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if AR search was called even for chain of calls
303
531
  ```ruby
304
532
  Group.includes(:some_relation).find(8)
305
533
  ```
306
- - works ignores where statements and still raises error
534
+
535
+ ![](https://placehold.it/15/f03c15/000000?text=+) ignores where statements and still raises error
307
536
  ```ruby
308
537
  Group.includes(:some_relation).where(name: "John").find(8)
538
+ ```
539
+
540
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores find method with passed block
541
+ ```ruby
542
+ MAPPING.find { |x| x == 42 }
543
+ ```
544
+
545
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores find method with passed multiline block
546
+ ```ruby
547
+
548
+ MAPPING.find do |x|
549
+ x == 42
550
+ end
551
+
309
552
  ```
310
553
  ## Ducalis::RaiseWithourErrorClass
311
554
 
312
555
  It's better to add exception class as raise argument. It will make easier to catch and process it later.
313
- - raise when `raise` called without exception class
556
+
557
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises when `raise` called without exception class
314
558
  ```ruby
315
559
  raise "Something went wrong"
316
560
  ```
317
- - works when `raise` called with exception class
561
+
562
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores when `raise` called with exception class
318
563
  ```ruby
319
564
  raise StandardError, "Something went wrong"
320
565
  ```
321
- - works when `raise` called with exception instance
566
+
567
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores when `raise` called with exception instance
322
568
  ```ruby
323
569
  raise StandardError.new("Something went wrong")
324
570
  ```
@@ -331,14 +577,16 @@ It will allow you to reuse this regex and provide instructions for others.
331
577
  CONST_NAME = %<constant>s # "%<example>s"
332
578
  %<fixed_string>s
333
579
  ```
334
- - raise if somewhere in code used regex which is not moved to const
580
+
581
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises if somewhere in code used regex which is not moved to const
335
582
  ```ruby
336
583
 
337
584
  name = "john"
338
585
  puts "hi" if name =~ /john/
339
586
 
340
587
  ```
341
- - accepts matching constants
588
+
589
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores matching constants
342
590
  ```ruby
343
591
 
344
592
  REGEX = /john/
@@ -346,14 +594,16 @@ name = "john"
346
594
  puts "hi" if name =~ REGEX
347
595
 
348
596
  ```
349
- - ignores named ruby constants
597
+
598
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores named ruby constants
350
599
  ```ruby
351
600
 
352
601
  name = "john"
353
602
  puts "hi" if name =~ /[[:alpha:]]/
354
603
 
355
604
  ```
356
- - ignores dynamic regexs
605
+
606
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores dynamic regexs
357
607
  ```ruby
358
608
 
359
609
  name = "john"
@@ -364,7 +614,8 @@ puts "hi" if name =~ /.{#{name.length}}/
364
614
 
365
615
  It's better for controllers to stay adherent to REST:
366
616
  http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/
367
- - raise for controllers with non-REST methods
617
+
618
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for controllers with non-REST methods
368
619
  ```ruby
369
620
 
370
621
  class MyController < ApplicationController
@@ -373,7 +624,8 @@ class MyController < ApplicationController
373
624
  end
374
625
 
375
626
  ```
376
- - doesn't raise for controllers with private non-REST methods
627
+
628
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores controllers with private non-REST methods
377
629
  ```ruby
378
630
 
379
631
  class MyController < ApplicationController
@@ -383,7 +635,8 @@ class MyController < ApplicationController
383
635
  end
384
636
 
385
637
  ```
386
- - doesn't raise for controllers with only REST methods
638
+
639
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores controllers with only REST methods
387
640
  ```ruby
388
641
 
389
642
  class MyController < ApplicationController
@@ -397,7 +650,8 @@ class MyController < ApplicationController
397
650
  end
398
651
 
399
652
  ```
400
- - doesn't raise for non-controllers with non-REST methods
653
+
654
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores non-controllers with non-REST methods
401
655
  ```ruby
402
656
 
403
657
  class MyClass
@@ -411,14 +665,16 @@ end
411
665
 
412
666
  Please, do not suppress RuboCop metrics, may be you can introduce some refactoring or another concept.
413
667
 
414
- - raises on RuboCop disable comments
668
+
669
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises on RuboCop disable comments
415
670
  ```ruby
416
671
 
417
672
  # rubocop:disable Metrics/ParameterLists
418
673
  def some_method(a, b, c, d, e, f); end
419
674
 
420
675
  ```
421
- - doesnt raise on comment without RuboCop disabling
676
+
677
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores comment without RuboCop disabling
422
678
  ```ruby
423
679
 
424
680
  # some meaningful comment
@@ -430,14 +686,16 @@ def some_method(a, b, c, d, e, f); end
430
686
  Please, do not use strings as arguments for %<method_name>s argument.
431
687
  It's hard to test, grep sources, code highlighting and so on.
432
688
  Consider using of symbols or lambdas for complex expressions.
433
- - raise for string if argument
689
+
690
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for string if argument
434
691
  ```ruby
435
692
 
436
693
  before_save :set_full_name,
437
694
  if: 'name_changed? || postfix_name_changed?'
438
695
 
439
696
  ```
440
- - doesnt raise for lambda if argument
697
+
698
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores lambda if argument
441
699
  ```ruby
442
700
  validates :file, if: -> { remote_url.blank? }
443
701
  ```
@@ -445,7 +703,8 @@ validates :file, if: -> { remote_url.blank? }
445
703
 
446
704
  Please, add comment why are you including non-realized gem version for %<gem>s.
447
705
  It will increase [bus-factor](<https://en.wikipedia.org/wiki/Bus_factor>).
448
- - raise for gem from github without comment
706
+
707
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for gem from github without comment
449
708
  ```ruby
450
709
 
451
710
  gem 'a'
@@ -453,7 +712,8 @@ gem 'b', '~> 1.3.1'
453
712
  gem 'c', git: 'https://github.com/c/c'
454
713
 
455
714
  ```
456
- - doesn't raise for gem from github with comment
715
+
716
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores for gem from github with comment
457
717
  ```ruby
458
718
 
459
719
  gem 'a'
@@ -475,7 +735,8 @@ def index
475
735
  do_something
476
736
  end
477
737
  ```
478
- - raises for `before_filters` with only one method as array
738
+
739
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for `before_filters` with only one method as array
479
740
  ```ruby
480
741
 
481
742
  class MyController < ApplicationController
@@ -486,7 +747,8 @@ class MyController < ApplicationController
486
747
  end
487
748
 
488
749
  ```
489
- - raises for `before_filters` with only one method as keyword array
750
+
751
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for `before_filters` with only one method as keyword array
490
752
  ```ruby
491
753
 
492
754
  class MyController < ApplicationController
@@ -497,7 +759,8 @@ class MyController < ApplicationController
497
759
  end
498
760
 
499
761
  ```
500
- - raises for `before_filters` with many actions and only one method
762
+
763
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for `before_filters` with many actions and only one method
501
764
  ```ruby
502
765
 
503
766
  class MyController < ApplicationController
@@ -509,7 +772,8 @@ class MyController < ApplicationController
509
772
  end
510
773
 
511
774
  ```
512
- - raises for `before_filters` with only one method as argument
775
+
776
+ ![](https://placehold.it/15/f03c15/000000?text=+) raises for `before_filters` with only one method as argument
513
777
  ```ruby
514
778
 
515
779
  class MyController < ApplicationController
@@ -520,7 +784,8 @@ class MyController < ApplicationController
520
784
  end
521
785
 
522
786
  ```
523
- - ignores `before_filters` without arguments
787
+
788
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores `before_filters` without arguments
524
789
  ```ruby
525
790
 
526
791
  class MyController < ApplicationController
@@ -531,7 +796,8 @@ class MyController < ApplicationController
531
796
  end
532
797
 
533
798
  ```
534
- - ignores `before_filters` with `only` and many arguments
799
+
800
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores `before_filters` with `only` and many arguments
535
801
  ```ruby
536
802
 
537
803
  class MyController < ApplicationController
@@ -543,7 +809,8 @@ class MyController < ApplicationController
543
809
  end
544
810
 
545
811
  ```
546
- - ignores `before_filters` with `except` and one argument
812
+
813
+ ![](https://placehold.it/15/2cbe4e/000000?text=+) ignores `before_filters` with `except` and one argument
547
814
  ```ruby
548
815
 
549
816
  class MyController < ApplicationController
data/Gemfile CHANGED
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
4
4
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
5
 
6
6
  gemspec
7
+
8
+ # Development dependencies
9
+ gem 'bundler', '~> 1.16.a'
10
+ gem 'pry', '~> 0.10', '>= 0.10.0'
11
+ gem 'rake', '~> 12.1'
12
+ gem 'rspec', '~> 3.0'
data/Gemfile.lock CHANGED
@@ -1,11 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ducalis (0.2.0)
4
+ ducalis (0.3.0)
5
5
  git (~> 1.3, >= 1.3.0)
6
6
  policial (= 0.0.4)
7
7
  regexp-examples (~> 1.3, >= 1.3.2)
8
- rubocop (~> 0.50.0)
9
8
  thor (~> 0.20.0)
10
9
 
11
10
  GEM
data/README.md CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ducalis.svg)](https://badge.fury.io/rb/ducalis)
4
4
  [![Build Status](https://travis-ci.org/ignat-z/ducalis.svg?branch=master)](https://travis-ci.org/ignat-z/ducalis)
5
+ [![Code Climate](https://codeclimate.com/github/ignat-z/ducalis/badges/gpa.svg)](https://codeclimate.com/github/ignat-z/ducalis)
5
6
 
6
- __Ducalis__ is RuboCop based static code analyzer for enterprise Rails applications.
7
+ __Ducalis__ is RuboCop-based static code analyzer for enterprise Rails applications.
8
+ As __Ducalis__ isn't style checker and could sometimes be false-positive it's not
9
+ necessary to follow all it rules, the main purpose of __Ducalis__ is help to find
10
+ possible weak code parts.
7
11
 
8
12
  ## Installation
9
13
 
@@ -16,3 +20,41 @@ gem 'ducalis'
16
20
  ## License
17
21
 
18
22
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
23
+
24
+ ## Usage
25
+
26
+ There are a lot of variants how you can use __Ducalis__:
27
+
28
+ 1. As CLI application. In this mode __Ducalis__ will notify you about any
29
+ possible violations in CLI.
30
+ ```
31
+ ducalis
32
+ ducalis app/controllers/
33
+ ```
34
+ As __Ducalis__ allows to pass build even with violations it's make sense to run
35
+ __Ducalis__ across current branch or index:
36
+ ```
37
+ ducalis --branch
38
+ ducalis --index
39
+ ```
40
+
41
+ 2. As CLI application in CI mode: In this mode __Ducalis__ will notify you about
42
+ any violations in your PR.
43
+ ```
44
+ ducalis --ci --repo="author/repo" --id=3575 --dry
45
+ ducalis --ci --repo="author/repo" --id=3575
46
+ ducalis --ci --adapter=circle # mode for running on CircleCI
47
+ ```
48
+ `--dry` option declares that output will be printed in console, if you will run
49
+ without this option __Ducalis__ will notify about violations in your PR.
50
+ _N.B._ You should provide GITHUB_TOKEN Env to allow __Ducalis__ download your PR
51
+ code and write review comments.
52
+
53
+ 3. As stand-alone server mode: In this mode __Ducalis__ will work as server,
54
+ listen webhooks from GitHub, and notify about any violations in PR. There is a
55
+ `Dockerfile` which you could use to run server for this or run it manually like
56
+ rack application. All related files are located in the `client/` directory.
57
+
58
+ In CLI modes you can provide yours `.ducalis.yml` file based on
59
+ [default](https://github.com/ignat-z/ducalis/blob/master/config/.ducalis.yml) by
60
+ `-c` flag or simply putting it in your project directory.
data/config/.ducalis.yml CHANGED
@@ -5,9 +5,15 @@ AllCops:
5
5
  - 'node_modules/**/*'
6
6
  - 'vendor/bundle/**/*'
7
7
 
8
+ Ducalis/CaseMapping:
9
+ Enabled: true
10
+
8
11
  Ducalis/CallbacksActiverecord:
9
12
  Enabled: true
10
13
 
14
+ Ducalis/PossibleTap:
15
+ Enabled: true
16
+
11
17
  Ducalis/ProtectedScopeCop:
12
18
  Enabled: true
13
19
 
@@ -39,6 +45,8 @@ Ducalis/UncommentedGem:
39
45
 
40
46
  Ducalis/ParamsPassing:
41
47
  Enabled: true
48
+ Exclude:
49
+ - 'spec/**/*.rb'
42
50
 
43
51
  Ducalis/ControllersExcept:
44
52
  Enabled: true
data/ducalis.gemspec CHANGED
@@ -29,13 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
30
 
31
31
  spec.add_dependency 'policial', '0.0.4'
32
- spec.add_dependency 'rubocop', '~> 0.50.0'
33
32
  spec.add_dependency 'regexp-examples', '~> 1.3', '>= 1.3.2'
34
33
  spec.add_dependency 'thor', '~> 0.20.0'
35
34
  spec.add_dependency 'git', '~> 1.3', '>= 1.3.0'
36
-
37
- spec.add_development_dependency 'bundler', '~> 1.16.a'
38
- spec.add_development_dependency 'rake', '~> 12.1'
39
- spec.add_development_dependency 'rspec', '~> 3.0'
40
- spec.add_development_dependency 'pry', '~> 0.10', '>= 0.10.0'
41
35
  end
data/lib/ducalis.rb CHANGED
@@ -32,10 +32,12 @@ require 'ducalis/patched_rubocop/git_turget_finder'
32
32
  require 'ducalis/patched_rubocop/rubo_cop'
33
33
 
34
34
  require 'ducalis/cops/callbacks_activerecord'
35
+ require 'ducalis/cops/case_mapping'
35
36
  require 'ducalis/cops/controllers_except'
36
37
  require 'ducalis/cops/keyword_defaults'
37
38
  require 'ducalis/cops/module_like_class'
38
39
  require 'ducalis/cops/params_passing'
40
+ require 'ducalis/cops/possible_tap'
39
41
  require 'ducalis/cops/private_instance_assign'
40
42
  require 'ducalis/cops/protected_scope_cop'
41
43
  require 'ducalis/cops/raise_withour_error_class'
@@ -4,6 +4,7 @@ module Ducalis
4
4
  module Commentators
5
5
  class Github
6
6
  STATUS = 'COMMENT'
7
+ SIMILARITY_THRESHOLD = 0.8
7
8
 
8
9
  def initialize(config)
9
10
  @config = config
@@ -26,13 +27,16 @@ module Ducalis
26
27
  [
27
28
  violation.filename == commented_violation[:path],
28
29
  violation.line.patch_position == commented_violation[:position],
29
- Utils.similarity(
30
- violation.message, commented_violation[:body]
31
- ) > 0.9
30
+ similar_messages?(violation.message, commented_violation[:body])
32
31
  ].all?
33
32
  end
34
33
  end
35
34
 
35
+ def similar_messages?(message, body)
36
+ body.include?(message) ||
37
+ Utils.similarity(message, body) > SIMILARITY_THRESHOLD
38
+ end
39
+
36
40
  def commented_violations
37
41
  @commented_violations ||=
38
42
  Utils.octokit.pull_request_comments(@config.repo, @config.id)
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class CaseMapping < RuboCop::Cop::Cop
7
+ OFFENSE = %(
8
+ Try to avoid `case when` statements. You can replace it with a sequence of
9
+ `if... elsif... elsif... else`. For cases where you need to choose from a
10
+ large number of possibilities, you can create a dictionary mapping case values
11
+ to functions to call by `call`. It's nice to have prefix for the method
12
+ names, i.e.: `visit_`.
13
+
14
+ <details>
15
+ Usually `case when` statements are using for the next reasons:
16
+
17
+ I. Mapping between different values.
18
+ ("A" => 1, "B" => 2, ...)
19
+
20
+ This case is all about data representing. If you do not need to execute any code
21
+ it's better to use data structure which represents it. This way you are
22
+ separating concepts: code returns corresponding value and you have config-like
23
+ data structure which describes your data.
24
+
25
+ ```ruby
26
+ %w[A B ...].index("A") + 1
27
+ # or
28
+ { "A" => 1, "B" => 2 }.fetch("A")
29
+ ```
30
+
31
+ II. Code execution depending of parameter or type:
32
+
33
+ - a. (:attack => attack, :defend => defend)
34
+ - b. (Feet => value * 0.348, Meters => `value`)
35
+
36
+ In this case code violates OOP and S[O]LID principle. Code shouldn't know about
37
+ object type and classes should be open for extension, but closed for
38
+ modification (but you can't do it with case-statements).
39
+ This is a signal that you have some problems with architecture.
40
+
41
+ a.
42
+ ```ruby
43
+ attack: -> { execute_attack }, defend: -> { execute_defend }
44
+ #{(action = '#{' + 'action' + '}') && '# or'}
45
+ call(:"execute_#{action}")
46
+ ```
47
+
48
+ b.
49
+ ```ruby
50
+ class Meters; def to_metters; value; end
51
+ class Feet; def to_metters; value * 0.348; end
52
+ ```
53
+
54
+ III. Code execution depending on some statement.
55
+ (`a > 0` => 1, `a == 0` => 0, `a < 0` => -1)
56
+
57
+ This case is combination of I and II -- high code complexity and unit-tests
58
+ complexity. There are variants how to solve it:
59
+
60
+ a. Rewrite to simple if statement
61
+
62
+ ```ruby
63
+ return 0 if a == 0
64
+ a > 0 ? 1 : -1
65
+ ```
66
+
67
+ b. Move statements to lambdas:
68
+
69
+ ```ruby
70
+ ->(a) { a > 0 } => 1,
71
+ ->(a) { a == 0 } => 0,
72
+ ->(a) { a < 0 } => -1
73
+ ```
74
+
75
+ This way decreases code complexity by delegating it to lambdas and makes it easy
76
+ to unit-testing because it's easy to test pure lambdas.
77
+
78
+ Such approach is named
79
+ [table-driven design](<https://www.d.umn.edu/~gshute/softeng/table-driven.html>)
80
+ . Table-driven methods are schemes that allow you to look up information in a
81
+ table rather than using logic statements (i.e. case, if). In simple cases,
82
+ it's quicker and easier to use logic statements, but as the logic chain becomes
83
+ more complex, table-driven code is simpler than complicated logic, easier to
84
+ modify and more efficient.
85
+ </details>
86
+ ).strip
87
+
88
+ def on_case(node)
89
+ add_offense(node, :expression, OFFENSE)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class PossibleTap < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+
9
+ OFFENSE = %(
10
+ Consider of using `.tap`, default ruby \
11
+ [method](<https://apidock.com/ruby/Object/tap>) which allows to replace \
12
+ intermediate variables with block, by this you are limiting scope pollution \
13
+ and make scope more clear. \
14
+ [Related article](<http://seejohncode.com/2012/01/02/ruby-tap-that/>).
15
+ ).strip
16
+
17
+ PAIRS = {
18
+ lvar: :lvasgn,
19
+ ivar: :ivasgn
20
+ }.freeze
21
+ ASSIGNS = PAIRS.keys
22
+
23
+ def on_def(node)
24
+ _name, _args, body = *node
25
+ return if body.nil?
26
+ return unless (possibe_var = return_var?(body) || return_var_call?(body))
27
+ return unless (assign_node = find_assign(body, possibe_var))
28
+ add_offense(assign_node, :expression, OFFENSE)
29
+ end
30
+
31
+ private
32
+
33
+ def unwrap_asign(node)
34
+ node.type == :or_asgn ? node.children.first : node
35
+ end
36
+
37
+ def find_assign(body, var_node)
38
+ subnodes(body).find do |subnode|
39
+ unwrap_asign(subnode).type == PAIRS[var_node.type] &&
40
+ unwrap_asign(subnode).to_a.first == var_node.to_a.first
41
+ end
42
+ end
43
+
44
+ def return_var?(body)
45
+ return unless body.children.last.respond_to?(:type)
46
+ return unless ASSIGNS.include?(body.children.last.type)
47
+ body.children.last
48
+ end
49
+
50
+ def return_var_call?(body)
51
+ return unless body.children.last.respond_to?(:children)
52
+ subnodes(body.children.last).find { |node| ASSIGNS.include?(node.type) }
53
+ end
54
+
55
+ def subnodes(node)
56
+ node.children.select { |child| child.respond_to?(:type) }
57
+ end
58
+ end
59
+ end
@@ -17,8 +17,9 @@ Employee.find(params[:id])
17
17
  }.strip
18
18
 
19
19
  def on_send(node)
20
- _, method_name, = *node
20
+ _, method_name, *args = *node
21
21
  return unless method_name == :find
22
+ return if args.empty?
22
23
  return unless children(node).any? { |subnode| subnode.type == :const }
23
24
  add_offense(node, :expression, OFFENSE)
24
25
  end
@@ -69,12 +69,20 @@ class Documentation
69
69
  ] +
70
70
  specs.map do |(it, code)|
71
71
  [
72
- "- #{it}", # case description
73
- "```ruby\n#{code.join("\n")}\n```" # code example
72
+ "\n#{color(it)} #{it}", # case description
73
+ "```ruby\n#{code.join("\n")}\n```" # code example
74
74
  ]
75
75
  end
76
76
  end
77
77
 
78
+ def color(it)
79
+ if it.include?('raises')
80
+ '![](https://placehold.it/15/f03c15/000000?text=+)'
81
+ else
82
+ '![](https://placehold.it/15/2cbe4e/000000?text=+)'
83
+ end
84
+ end
85
+
78
86
  def spec_cases_for(f)
79
87
  source_code = File.read(
80
88
  f.sub('/lib/ducalis/', '/spec/')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ducalis
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ducalis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ignat Zakrevsky
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-07 00:00:00.000000000 Z
11
+ date: 2017-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: policial
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.0.4
27
- - !ruby/object:Gem::Dependency
28
- name: rubocop
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 0.50.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 0.50.0
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: regexp-examples
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -92,68 +78,6 @@ dependencies:
92
78
  - - ">="
93
79
  - !ruby/object:Gem::Version
94
80
  version: 1.3.0
95
- - !ruby/object:Gem::Dependency
96
- name: bundler
97
- requirement: !ruby/object:Gem::Requirement
98
- requirements:
99
- - - "~>"
100
- - !ruby/object:Gem::Version
101
- version: 1.16.a
102
- type: :development
103
- prerelease: false
104
- version_requirements: !ruby/object:Gem::Requirement
105
- requirements:
106
- - - "~>"
107
- - !ruby/object:Gem::Version
108
- version: 1.16.a
109
- - !ruby/object:Gem::Dependency
110
- name: rake
111
- requirement: !ruby/object:Gem::Requirement
112
- requirements:
113
- - - "~>"
114
- - !ruby/object:Gem::Version
115
- version: '12.1'
116
- type: :development
117
- prerelease: false
118
- version_requirements: !ruby/object:Gem::Requirement
119
- requirements:
120
- - - "~>"
121
- - !ruby/object:Gem::Version
122
- version: '12.1'
123
- - !ruby/object:Gem::Dependency
124
- name: rspec
125
- requirement: !ruby/object:Gem::Requirement
126
- requirements:
127
- - - "~>"
128
- - !ruby/object:Gem::Version
129
- version: '3.0'
130
- type: :development
131
- prerelease: false
132
- version_requirements: !ruby/object:Gem::Requirement
133
- requirements:
134
- - - "~>"
135
- - !ruby/object:Gem::Version
136
- version: '3.0'
137
- - !ruby/object:Gem::Dependency
138
- name: pry
139
- requirement: !ruby/object:Gem::Requirement
140
- requirements:
141
- - - "~>"
142
- - !ruby/object:Gem::Version
143
- version: '0.10'
144
- - - ">="
145
- - !ruby/object:Gem::Version
146
- version: 0.10.0
147
- type: :development
148
- prerelease: false
149
- version_requirements: !ruby/object:Gem::Requirement
150
- requirements:
151
- - - "~>"
152
- - !ruby/object:Gem::Version
153
- version: '0.10'
154
- - - ">="
155
- - !ruby/object:Gem::Version
156
- version: 0.10.0
157
81
  description: " Ducalis is RuboCop based static code analyzer for enterprise Rails
158
82
  \ applications.\n"
159
83
  email:
@@ -163,6 +87,7 @@ executables:
163
87
  extensions: []
164
88
  extra_rdoc_files: []
165
89
  files:
90
+ - ".codeclimate.yml"
166
91
  - ".gitignore"
167
92
  - ".rspec"
168
93
  - ".rubocop.yml"
@@ -186,10 +111,12 @@ files:
186
111
  - lib/ducalis/commentators/console.rb
187
112
  - lib/ducalis/commentators/github.rb
188
113
  - lib/ducalis/cops/callbacks_activerecord.rb
114
+ - lib/ducalis/cops/case_mapping.rb
189
115
  - lib/ducalis/cops/controllers_except.rb
190
116
  - lib/ducalis/cops/keyword_defaults.rb
191
117
  - lib/ducalis/cops/module_like_class.rb
192
118
  - lib/ducalis/cops/params_passing.rb
119
+ - lib/ducalis/cops/possible_tap.rb
193
120
  - lib/ducalis/cops/private_instance_assign.rb
194
121
  - lib/ducalis/cops/protected_scope_cop.rb
195
122
  - lib/ducalis/cops/raise_withour_error_class.rb