ducalis 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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