breadkit-lint 0.1.0 → 0.2.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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.yamllint +16 -0
  3. data/CHANGELOG.md +30 -2
  4. data/README.md +55 -40
  5. data/action.yml +42 -0
  6. data/config/default.yml +33 -0
  7. data/docs/REFERENCE.md +129 -0
  8. data/docs/rules/Electrical/CapacitorVoltageRating.md +5 -0
  9. data/docs/rules/Electrical/FloatingInput.md +11 -0
  10. data/docs/rules/Electrical/GpioOvercurrent.md +18 -0
  11. data/docs/rules/Electrical/I2CAddressConflict.md +5 -0
  12. data/docs/rules/Electrical/I2CPullupMissing.md +5 -0
  13. data/docs/rules/Electrical/LedOvercurrent.md +5 -0
  14. data/docs/rules/Electrical/MinimumResistance.md +14 -0
  15. data/docs/rules/Electrical/MissingBaseResistor.md +5 -0
  16. data/docs/rules/Electrical/MissingDecouplingCapacitor.md +5 -0
  17. data/docs/rules/Electrical/MissingFlybackDiode.md +18 -0
  18. data/docs/rules/Electrical/MissingPullResistor.md +17 -0
  19. data/docs/rules/Electrical/MissingSeriesResistor.md +3 -1
  20. data/docs/rules/Electrical/NoCommonGround.md +1 -0
  21. data/docs/rules/Electrical/RailPolarityMismatch.md +10 -0
  22. data/docs/rules/Electrical/ResistorPowerRating.md +5 -0
  23. data/docs/rules/Electrical/ReversePolarity.md +1 -1
  24. data/docs/rules/Electrical/SupplyOverload.md +11 -0
  25. data/docs/rules/Electrical/SupplyVoltageRange.md +1 -0
  26. data/docs/rules/Electrical/VoltageDomainMismatch.md +6 -0
  27. data/docs/rules/Intent/ConnectionMismatch.md +3 -1
  28. data/docs/rules/Intent/MeasurementUnavailable.md +10 -0
  29. data/docs/rules/Layout/AmbiguousSupplySource.md +5 -0
  30. data/docs/rules/Layout/BodyOverlap.md +11 -0
  31. data/docs/rules/Layout/HoleCovered.md +11 -0
  32. data/docs/rules/Layout/InvalidColor.md +3 -0
  33. data/docs/rules/Layout/InvalidOption.md +3 -0
  34. data/docs/rules/Layout/InvalidRoute.md +3 -0
  35. data/docs/rules/Layout/InvalidValue.md +3 -0
  36. data/docs/rules/Layout/InvalidWireId.md +3 -0
  37. data/docs/rules/Layout/LeadSpan.md +21 -0
  38. data/docs/rules/Layout/PartOverride.md +3 -0
  39. data/docs/rules/Layout/UnknownSupplySource.md +5 -0
  40. data/docs/rules/Layout/UnmatchedParts.md +3 -0
  41. data/docs/rules/Layout/WireOverIC.md +11 -0
  42. data/docs/rules/Lint/RedundantDisable.md +3 -0
  43. data/docs/rules/Lint/UnknownRuleInDisable.md +5 -0
  44. data/lib/breadkit/lint/baseline.rb +38 -0
  45. data/lib/breadkit/lint/rake_task.rb +22 -0
  46. data/lib/breadkit/lint/rules/electrical.rb +580 -30
  47. data/lib/breadkit/lint/rules/intent.rb +69 -4
  48. data/lib/breadkit/lint/rules/layout.rb +148 -0
  49. data/lib/breadkit/lint/rules/style.rb +36 -7
  50. data/lib/breadkit/lint/rules/support.rb +45 -11
  51. data/lib/breadkit/lint/rules.rb +205 -2
  52. data/lib/breadkit/lint/source_editor.rb +138 -0
  53. data/lib/breadkit/lint/version.rb +1 -1
  54. data/lib/breadkit/lint.rb +422 -59
  55. data/lib/guard/breadkit/templates/Guardfile +7 -0
  56. data/lib/guard/breadkit.rb +26 -0
  57. data/locales/en.yml +82 -0
  58. data/locales/ja.yml +130 -4
  59. data/locales/ko.yml +191 -0
  60. data/locales/zh.yml +191 -0
  61. data/package-lock.json +103 -0
  62. data/package.json +2 -1
  63. data/scripts/action-run.sh +51 -0
  64. data/scripts/build-rule-pages.mjs +87 -0
  65. data/scripts/check-rule-pages.mjs +18 -0
  66. data/scripts/pre-commit +24 -0
  67. data/sig/breadkit/lint.rbs +12 -1
  68. data/site/index.html +9 -6
  69. data/site/schemas/lint-v1.json +71 -0
  70. data/site/styles.css +13 -0
  71. metadata +64 -4
@@ -24,6 +24,20 @@ module Breadkit
24
24
  end
25
25
 
26
26
  module Rules
27
+ module Lint
28
+ class UnknownRuleInDisable < BuiltinRule
29
+ rule "Lint/UnknownRuleInDisable", severity: :error, description: "A lint_disable names an unknown rule"
30
+
31
+ def check(_context); end
32
+ end
33
+
34
+ class RedundantDisable < BuiltinRule
35
+ rule "Lint/RedundantDisable", severity: :warning, description: "A lint_disable matches no offense"
36
+
37
+ def check(_context); end
38
+ end
39
+ end
40
+
27
41
  module Layout
28
42
  class InvalidHole < DiagnosticRule
29
43
  rule "Layout/InvalidHole", severity: :error, description: "Unknown board hole"
@@ -55,12 +69,66 @@ module Breadkit
55
69
  def check(context) = emit_diagnostics(context)
56
70
  end
57
71
 
72
+ class UnknownSupplySource < DiagnosticRule
73
+ rule "Layout/UnknownSupplySource", severity: :error, description: "Supply source is not a provided power output"
74
+
75
+ def check(context) = emit_diagnostics(context)
76
+ end
77
+
78
+ class AmbiguousSupplySource < DiagnosticRule
79
+ rule "Layout/AmbiguousSupplySource", severity: :error, description: "Supply source matches multiple power outputs"
80
+
81
+ def check(context) = emit_diagnostics(context)
82
+ end
83
+
58
84
  class UnknownOption < DiagnosticRule
59
85
  rule "Layout/UnknownOption", severity: :error, description: "Unknown component option"
60
86
 
61
87
  def check(context) = emit_diagnostics(context)
62
88
  end
63
89
 
90
+ class InvalidOption < DiagnosticRule
91
+ rule "Layout/InvalidOption", severity: :error, description: "Invalid component option value"
92
+
93
+ def check(context) = emit_diagnostics(context)
94
+ end
95
+
96
+ class InvalidValue < DiagnosticRule
97
+ rule "Layout/InvalidValue", severity: :error, description: "Invalid component value"
98
+
99
+ def check(context) = emit_diagnostics(context)
100
+ end
101
+
102
+ class InvalidColor < DiagnosticRule
103
+ rule "Layout/InvalidColor", severity: :error, description: "Invalid component or wire color"
104
+
105
+ def check(context) = emit_diagnostics(context)
106
+ end
107
+
108
+ class InvalidRoute < DiagnosticRule
109
+ rule "Layout/InvalidRoute", severity: :error, description: "Invalid wire route"
110
+
111
+ def check(context) = emit_diagnostics(context)
112
+ end
113
+
114
+ class InvalidWireId < DiagnosticRule
115
+ rule "Layout/InvalidWireId", severity: :error, description: "Wire ID conflicts with a board hole"
116
+
117
+ def check(context) = emit_diagnostics(context)
118
+ end
119
+
120
+ class UnmatchedParts < DiagnosticRule
121
+ rule "Layout/UnmatchedParts", severity: :warning, description: "Part glob matches no files"
122
+
123
+ def check(context) = emit_diagnostics(context)
124
+ end
125
+
126
+ class PartOverride < DiagnosticRule
127
+ rule "Layout/PartOverride", severity: :warning, description: "Custom part overrides a built-in part"
128
+
129
+ def check(context) = emit_diagnostics(context)
130
+ end
131
+
64
132
  class UnplacedPin < DiagnosticRule
65
133
  rule "Layout/UnplacedPin", severity: :error, description: "A component pin has no board hole"
66
134
 
@@ -85,6 +153,30 @@ module Breadkit
85
153
  def check(context) = emit_diagnostics(context)
86
154
  end
87
155
 
156
+ class HoleCovered < BuiltinRule
157
+ rule "Layout/HoleCovered", severity: :error, description: "A module or DIP body covers another lead or wire endpoint"
158
+
159
+ def check(context) = emit(context, :hole_covered)
160
+ end
161
+
162
+ class BodyOverlap < BuiltinRule
163
+ rule "Layout/BodyOverlap", severity: :error, description: "Two component bodies overlap"
164
+
165
+ def check(context) = emit(context, :body_overlap)
166
+ end
167
+
168
+ class WireOverIC < BuiltinRule
169
+ rule "Layout/WireOverIC", severity: :info, description: "A straight jumper crosses a DIP IC body"
170
+
171
+ def check(context) = emit(context, :wire_over_ic)
172
+ end
173
+
174
+ class LeadSpan < BuiltinRule
175
+ rule "Layout/LeadSpan", severity: :error, description: "A component's leads cannot span the placed holes"
176
+
177
+ def check(context) = emit(context, :lead_span)
178
+ end
179
+
88
180
  class NoFreeHole < DiagnosticRule
89
181
  rule "Layout/NoFreeHole", severity: :error, description: "No free hole is available"
90
182
 
@@ -124,6 +216,20 @@ module Breadkit
124
216
  def check(context) = emit(context, :floating_pins)
125
217
  end
126
218
 
219
+ class FloatingInput < BuiltinRule
220
+ rule "Electrical/FloatingInput", severity: :info,
221
+ description: "An input net has no modeled drive or pull resistor", state_sensitive: true
222
+
223
+ def check(context) = emit(context, :floating_inputs)
224
+ end
225
+
226
+ class MissingPullResistor < BuiltinRule
227
+ rule "Electrical/MissingPullResistor", severity: :info,
228
+ description: "A supply-switched input has no modeled pull resistor"
229
+
230
+ def check(context) = emit(context, :missing_pull_resistors)
231
+ end
232
+
127
233
  class DanglingWire < BuiltinRule
128
234
  rule "Electrical/DanglingWire", severity: :warning, description: "A wire end has no other connection"
129
235
 
@@ -142,6 +248,20 @@ module Breadkit
142
248
  def check(context) = emit(context, :missing_series_resistors)
143
249
  end
144
250
 
251
+ class MinimumResistance < BuiltinRule
252
+ rule "Electrical/MinimumResistance", severity: :warning,
253
+ description: "A potentiometer can reach zero resistance in an LED current path", state_sensitive: true
254
+
255
+ def check(context) = emit(context, :minimum_resistances)
256
+ end
257
+
258
+ class RailPolarityMismatch < BuiltinRule
259
+ rule "Electrical/RailPolarityMismatch", severity: :warning,
260
+ description: "A source's positive and negative terminals drive oppositely marked rails"
261
+
262
+ def check(context) = emit(context, :rail_polarity_mismatch)
263
+ end
264
+
145
265
  class ReversePolarity < BuiltinRule
146
266
  rule "Electrical/ReversePolarity", severity: :error, description: "A polarized part is connected backwards", state_sensitive: true
147
267
 
@@ -172,17 +292,94 @@ module Breadkit
172
292
  def check(context) = emit(context, :label_conflicts)
173
293
  end
174
294
 
295
+ class VoltageDomainMismatch < BuiltinRule
296
+ rule "Electrical/VoltageDomainMismatch", severity: :error,
297
+ description: "A signal exceeds a pin's voltage limit", state_sensitive: true
298
+
299
+ def check(context) = emit(context, :voltage_domain_mismatches)
300
+ end
301
+
302
+ class ResistorPowerRating < BuiltinRule
303
+ rule "Electrical/ResistorPowerRating", severity: :error,
304
+ description: "A resistor exceeds its declared power rating", state_sensitive: true
305
+
306
+ def check(context) = emit(context, :resistor_power_ratings)
307
+ end
308
+
309
+ class CapacitorVoltageRating < BuiltinRule
310
+ rule "Electrical/CapacitorVoltageRating", severity: :error,
311
+ description: "An electrolytic exceeds its declared voltage rating", state_sensitive: true
312
+
313
+ def check(context) = emit(context, :capacitor_voltage_ratings)
314
+ end
315
+
316
+ class LedOvercurrent < BuiltinRule
317
+ rule "Electrical/LedOvercurrent", severity: :error,
318
+ description: "An LED exceeds its declared current limit", state_sensitive: true
319
+
320
+ def check(context) = emit(context, :led_overcurrent)
321
+ end
322
+
323
+ class GpioOvercurrent < BuiltinRule
324
+ rule "Electrical/GpioOvercurrent", severity: :error,
325
+ description: "A modeled output current exceeds a pin's declared limit", state_sensitive: true
326
+
327
+ def check(context) = emit(context, :gpio_overcurrent)
328
+ end
329
+
330
+ class SupplyOverload < BuiltinRule
331
+ rule "Electrical/SupplyOverload", severity: :error,
332
+ description: "A modeled load exceeds its supply current limit", state_sensitive: true
333
+
334
+ def check(context) = emit(context, :supply_overloads)
335
+ end
336
+
337
+ class I2CAddressConflict < BuiltinRule
338
+ rule "Electrical/I2CAddressConflict", severity: :error,
339
+ description: "I2C devices on one bus use the same address", state_sensitive: true
340
+
341
+ def check(context) = emit(context, :i2c_address_conflicts)
342
+ end
343
+
344
+ class I2CPullupMissing < BuiltinRule
345
+ rule "Electrical/I2CPullupMissing", severity: :info,
346
+ description: "An I2C bus has no modeled pull-up resistor", state_sensitive: true
347
+
348
+ def check(context) = emit(context, :i2c_pullup_missing)
349
+ end
350
+
351
+ class MissingDecouplingCapacitor < BuiltinRule
352
+ rule "Electrical/MissingDecouplingCapacitor", severity: :info,
353
+ description: "A powered IC has no modeled capacitor across its supply pins", state_sensitive: true
354
+
355
+ def check(context) = emit(context, :missing_decoupling_capacitors)
356
+ end
357
+
358
+ class MissingBaseResistor < BuiltinRule
359
+ rule "Electrical/MissingBaseResistor", severity: :info,
360
+ description: "A declared output drives a transistor base without a series resistor", state_sensitive: true
361
+
362
+ def check(context) = emit(context, :missing_base_resistors)
363
+ end
364
+
365
+ class MissingFlybackDiode < BuiltinRule
366
+ rule "Electrical/MissingFlybackDiode", severity: :warning,
367
+ description: "An explicitly inductive load has no reverse flyback diode", state_sensitive: true
368
+
369
+ def check(context) = emit(context, :missing_flyback_diodes)
370
+ end
371
+
175
372
  end
176
373
 
177
374
  module Intent
178
375
  class ConnectionMismatch < BuiltinRule
179
- rule "Intent/ConnectionMismatch", severity: :error, description: "Wiring differs from declared expectations"
376
+ rule "Intent/ConnectionMismatch", severity: :error, description: "Wiring differs from declared expectations", state_sensitive: true
180
377
 
181
378
  def check(context) = emit(context, :expectations)
182
379
  end
183
380
 
184
381
  class UnknownNet < BuiltinRule
185
- rule "Intent/UnknownNet", severity: :error, description: "An expectation refers to an unknown net"
382
+ rule "Intent/UnknownNet", severity: :error, description: "An expectation refers to an unknown net", state_sensitive: true
186
383
 
187
384
  def check(context)
188
385
  emit_diagnostics(context)
@@ -190,6 +387,12 @@ module Breadkit
190
387
  end
191
388
  end
192
389
 
390
+ class MeasurementUnavailable < BuiltinRule
391
+ rule "Intent/MeasurementUnavailable", severity: :warning, description: "A requested DC value cannot be calculated", state_sensitive: true
392
+
393
+ def check(context) = emit(context, :expectations)
394
+ end
395
+
193
396
  end
194
397
 
195
398
  module Style
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require "tempfile"
5
+ require "did_you_mean"
6
+
7
+ module Breadkit
8
+ module Lint
9
+ class SourceEditor
10
+ Edit = Struct.new(:start, :finish, :replacement, :description, :line, keyword_init: true)
11
+
12
+ def initialize(path, source: nil)
13
+ @path = path
14
+ @source = source || File.binread(path)
15
+ parsed = Prism.parse(@source)
16
+ @valid = parsed.errors.empty?
17
+ @calls = []
18
+ collect(parsed.value) if @valid
19
+ @calls_by_line = @calls.group_by { |call| call.location.start_line }
20
+ end
21
+
22
+ def annotate(offenses)
23
+ return unless @valid
24
+
25
+ offenses.each do |offense|
26
+ call = call_for(offense)
27
+ next unless call
28
+ node = offense.rule == "Layout/InvalidColor" && wire_color_node(call) || call
29
+ offense.column = node.location.start_character_column + 1
30
+ if node != call && node.location.start_line != offense.location.line
31
+ offense.location = Breadkit::SourceLocation.new(path: offense.location.path, line: node.location.start_line)
32
+ end
33
+ end
34
+ end
35
+
36
+ def plan(offenses)
37
+ return [] unless @valid
38
+
39
+ offenses.filter_map do |offense|
40
+ case offense.rule
41
+ when "Layout/InvalidColor" then color_edit(offense)
42
+ when "Lint/RedundantDisable" then disable_edit(offense)
43
+ end
44
+ end.uniq { |edit| [edit.start, edit.finish, edit.replacement] }.sort_by(&:start)
45
+ end
46
+
47
+ def apply(edits)
48
+ return false if edits.empty? || File.symlink?(@path)
49
+ raise Error, "source changed while fixing #{@path}" unless File.binread(@path) == @source
50
+
51
+ updated = @source.dup
52
+ edits.reverse_each { |edit| updated[edit.start...edit.finish] = edit.replacement }
53
+ mode = File.stat(@path).mode & 0o7777
54
+ Tempfile.create([".bklint-fix-", ".tmp"], File.dirname(@path)) do |temp|
55
+ temp.binmode
56
+ temp.chmod(mode)
57
+ temp.write(updated)
58
+ temp.flush
59
+ temp.fsync
60
+ temp.close
61
+ raise Error, "source changed while fixing #{@path}" unless File.binread(@path) == @source
62
+ File.rename(temp.path, @path)
63
+ end
64
+ true
65
+ end
66
+
67
+ private
68
+
69
+ def collect(node)
70
+ return unless node
71
+ @calls << node if node.is_a?(Prism::CallNode) && node.receiver.nil?
72
+ node.compact_child_nodes.each { |child| collect(child) }
73
+ end
74
+
75
+ def call_for(offense, name = nil)
76
+ location = offense.location
77
+ return unless location&.line && location.path && File.expand_path(location.path) == File.expand_path(@path)
78
+
79
+ calls = @calls_by_line[location.line] || @calls.select do |call|
80
+ call.location.start_line < location.line && location.line <= call.location.end_line
81
+ end
82
+ return unless calls.one?
83
+ return if name && calls.first.name != name
84
+
85
+ calls.first
86
+ end
87
+
88
+ def wire_color_node(call)
89
+ return unless call.name == :wire
90
+ hashes = Array(call.arguments&.arguments).grep(Prism::KeywordHashNode)
91
+ entries = hashes.flat_map(&:elements).grep(Prism::AssocNode).select do |entry|
92
+ entry.key.is_a?(Prism::SymbolNode) && entry.key.value == "color"
93
+ end
94
+ entries.first.value if entries.one?
95
+ end
96
+
97
+ def color_edit(offense)
98
+ return if Array(offense.targets[:wires]).empty?
99
+ call = call_for(offense, :wire)
100
+ return unless call
101
+ node = wire_color_node(call)
102
+ return unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
103
+
104
+ color = node.is_a?(Prism::StringNode) ? node.unescaped : node.value
105
+ return unless color.match?(/\A[a-z]+\z/i) && !Breadkit::Color.valid?(color)
106
+ candidates = Breadkit::Color::NAMES.select { |name| DidYouMean::Levenshtein.distance(color.downcase, name) == 1 }
107
+ return unless candidates.one?
108
+
109
+ original = node.location.slice
110
+ corrected = if node.is_a?(Prism::SymbolNode) && original == ":#{color}"
111
+ ":#{candidates.first}"
112
+ elsif node.is_a?(Prism::StringNode) && original.match?(/\A(["'])[a-z]+\1\z/i)
113
+ "#{original[0]}#{candidates.first}#{original[0]}"
114
+ end
115
+ return unless corrected
116
+
117
+ Edit.new(start: node.location.start_offset, finish: node.location.end_offset, replacement: corrected,
118
+ description: "replace wire color #{original} with #{corrected}", line: offense.location.line)
119
+ end
120
+
121
+ def disable_edit(offense)
122
+ call = call_for(offense, :lint_disable)
123
+ return unless call && call.location.start_line == call.location.end_line
124
+
125
+ line_start = @source.lines.take(call.location.start_line - 1).sum(&:bytesize)
126
+ newline = @source.index("\n", line_start)
127
+ line_end = newline ? newline + 1 : @source.bytesize
128
+ content_end = newline && @source.getbyte(newline - 1) == 13 ? newline - 1 : (newline || line_end)
129
+ before = @source.byteslice(line_start...call.location.start_offset)
130
+ after = @source.byteslice(call.location.end_offset...content_end)
131
+ return unless before.match?(/\A[ \t]*\z/) && after.match?(/\A[ \t]*\z/)
132
+
133
+ Edit.new(start: line_start, finish: line_end, replacement: "",
134
+ description: "remove unused lint_disable", line: offense.location.line)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Breadkit
4
4
  module Lint
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end