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.
- checksums.yaml +4 -4
- data/.yamllint +16 -0
- data/CHANGELOG.md +30 -2
- data/README.md +55 -40
- data/action.yml +42 -0
- data/config/default.yml +33 -0
- data/docs/REFERENCE.md +129 -0
- data/docs/rules/Electrical/CapacitorVoltageRating.md +5 -0
- data/docs/rules/Electrical/FloatingInput.md +11 -0
- data/docs/rules/Electrical/GpioOvercurrent.md +18 -0
- data/docs/rules/Electrical/I2CAddressConflict.md +5 -0
- data/docs/rules/Electrical/I2CPullupMissing.md +5 -0
- data/docs/rules/Electrical/LedOvercurrent.md +5 -0
- data/docs/rules/Electrical/MinimumResistance.md +14 -0
- data/docs/rules/Electrical/MissingBaseResistor.md +5 -0
- data/docs/rules/Electrical/MissingDecouplingCapacitor.md +5 -0
- data/docs/rules/Electrical/MissingFlybackDiode.md +18 -0
- data/docs/rules/Electrical/MissingPullResistor.md +17 -0
- data/docs/rules/Electrical/MissingSeriesResistor.md +3 -1
- data/docs/rules/Electrical/NoCommonGround.md +1 -0
- data/docs/rules/Electrical/RailPolarityMismatch.md +10 -0
- data/docs/rules/Electrical/ResistorPowerRating.md +5 -0
- data/docs/rules/Electrical/ReversePolarity.md +1 -1
- data/docs/rules/Electrical/SupplyOverload.md +11 -0
- data/docs/rules/Electrical/SupplyVoltageRange.md +1 -0
- data/docs/rules/Electrical/VoltageDomainMismatch.md +6 -0
- data/docs/rules/Intent/ConnectionMismatch.md +3 -1
- data/docs/rules/Intent/MeasurementUnavailable.md +10 -0
- data/docs/rules/Layout/AmbiguousSupplySource.md +5 -0
- data/docs/rules/Layout/BodyOverlap.md +11 -0
- data/docs/rules/Layout/HoleCovered.md +11 -0
- data/docs/rules/Layout/InvalidColor.md +3 -0
- data/docs/rules/Layout/InvalidOption.md +3 -0
- data/docs/rules/Layout/InvalidRoute.md +3 -0
- data/docs/rules/Layout/InvalidValue.md +3 -0
- data/docs/rules/Layout/InvalidWireId.md +3 -0
- data/docs/rules/Layout/LeadSpan.md +21 -0
- data/docs/rules/Layout/PartOverride.md +3 -0
- data/docs/rules/Layout/UnknownSupplySource.md +5 -0
- data/docs/rules/Layout/UnmatchedParts.md +3 -0
- data/docs/rules/Layout/WireOverIC.md +11 -0
- data/docs/rules/Lint/RedundantDisable.md +3 -0
- data/docs/rules/Lint/UnknownRuleInDisable.md +5 -0
- data/lib/breadkit/lint/baseline.rb +38 -0
- data/lib/breadkit/lint/rake_task.rb +22 -0
- data/lib/breadkit/lint/rules/electrical.rb +580 -30
- data/lib/breadkit/lint/rules/intent.rb +69 -4
- data/lib/breadkit/lint/rules/layout.rb +148 -0
- data/lib/breadkit/lint/rules/style.rb +36 -7
- data/lib/breadkit/lint/rules/support.rb +45 -11
- data/lib/breadkit/lint/rules.rb +205 -2
- data/lib/breadkit/lint/source_editor.rb +138 -0
- data/lib/breadkit/lint/version.rb +1 -1
- data/lib/breadkit/lint.rb +422 -59
- data/lib/guard/breadkit/templates/Guardfile +7 -0
- data/lib/guard/breadkit.rb +26 -0
- data/locales/en.yml +82 -0
- data/locales/ja.yml +130 -4
- data/locales/ko.yml +191 -0
- data/locales/zh.yml +191 -0
- data/package-lock.json +103 -0
- data/package.json +2 -1
- data/scripts/action-run.sh +51 -0
- data/scripts/build-rule-pages.mjs +87 -0
- data/scripts/check-rule-pages.mjs +18 -0
- data/scripts/pre-commit +24 -0
- data/sig/breadkit/lint.rbs +12 -1
- data/site/index.html +9 -6
- data/site/schemas/lint-v1.json +71 -0
- data/site/styles.css +13 -0
- metadata +64 -4
|
@@ -7,19 +7,33 @@ module Breadkit
|
|
|
7
7
|
circuit.nets(state).filter_map do |net|
|
|
8
8
|
next unless net.labels.length > 1
|
|
9
9
|
message = translate("label_conflict", "net has conflicting labels: #{net.labels.join(', ')}", labels: net.labels.join(", "))
|
|
10
|
-
|
|
10
|
+
location = circuit.labels.find { |label| net.labels.include?(label.name) }&.location
|
|
11
|
+
offense(rule.id, message, location, targets: { nets: [net.name] }, state: state.name)
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def expectations(circuit, rule, state)
|
|
15
16
|
return [] if rule.id == "Intent/UnknownNet" && circuit.diagnostics.any? { |item| item.code == "unknown_net" }
|
|
16
17
|
circuit.expectations.flat_map do |expectation|
|
|
18
|
+
at_state = expectation["when"] || expectation[:when]
|
|
19
|
+
if at_state
|
|
20
|
+
unless @config.intent_states(circuit, [expectation]).any? { |candidate| candidate.name == at_state }
|
|
21
|
+
next [] unless state.name.nil? && rule.id == "Intent/ConnectionMismatch"
|
|
22
|
+
next [offense(rule.id, translate("unknown_state", "unknown circuit state #{at_state}", state: at_state),
|
|
23
|
+
location_from(expectation["location"] || expectation[:location]), targets: {})]
|
|
24
|
+
end
|
|
25
|
+
next [] unless state.name == at_state
|
|
26
|
+
else
|
|
27
|
+
next [] unless state.name.nil?
|
|
28
|
+
end
|
|
17
29
|
strict = expectation["strict"] || expectation[:strict]
|
|
18
|
-
Array(expectation["entries"] || expectation[:entries])
|
|
30
|
+
entries = Array(expectation["entries"] || expectation[:entries])
|
|
31
|
+
results = entries.filter_map do |item|
|
|
19
32
|
kind = item["kind"] || item[:kind]
|
|
20
33
|
refs = item["refs"] || item[:refs] || []
|
|
21
34
|
expected_name = item["name"] || item[:name]
|
|
22
35
|
loc = item["location"] || item[:location]
|
|
36
|
+
next measured_expectation(circuit, rule, state, item) if %w[voltage current].include?(kind)
|
|
23
37
|
names = refs.map(&:to_s)
|
|
24
38
|
nets = names.empty? && kind == "net" ? [circuit.net_of(expected_name, state)] : names.map { |reference| circuit.net_of(reference, state) }
|
|
25
39
|
if nets.any?(&:nil?)
|
|
@@ -38,20 +52,71 @@ module Breadkit
|
|
|
38
52
|
circuit.components.keys.any? { |ref| member.start_with?("#{ref}.") }
|
|
39
53
|
end.uniq
|
|
40
54
|
declared_pins = names.map { |name| canonical_pin(circuit, name) }
|
|
41
|
-
extra_pins = strict ? actual_pins - declared_pins : []
|
|
55
|
+
extra_pins = strict ? (actual_pins - declared_pins).sort : []
|
|
42
56
|
name_matches = nets.first.name == expected_name.to_s
|
|
43
57
|
next if nets.map(&:name).uniq.length == 1 && extra_pins.empty? && name_matches
|
|
44
58
|
detail = if !extra_pins.empty?
|
|
45
59
|
translate("undeclared_pins", "undeclared pins on expected net: #{extra_pins.join(', ')}", pins: extra_pins.join(", "))
|
|
46
60
|
elsif !name_matches
|
|
47
|
-
translate("expected_net_name", "expected net #{expected_name}, found #{nets.first.name}", expected: expected_name, actual: nets.first.name)
|
|
61
|
+
translate("expected_net_name", "expected net #{expected_name}, found #{nets.first.name}; add net :#{expected_name}, at: ... to label it", expected: expected_name, actual: nets.first.name)
|
|
48
62
|
else
|
|
49
63
|
translate("expected_one_net", "expected #{names.join(', ')} on one net", refs: names.join(", "))
|
|
50
64
|
end
|
|
51
65
|
offense(rule.id, detail, location_from(loc), targets: { nets: nets.compact.map(&:name), components: extra_pins.map { |pin| pin.split('.', 2).first } }, state: state.name)
|
|
52
66
|
end
|
|
53
67
|
end
|
|
68
|
+
if strict && rule.id == "Intent/ConnectionMismatch"
|
|
69
|
+
declared_names = entries.select { |item| (item["kind"] || item[:kind]) == "net" }.flat_map do |item|
|
|
70
|
+
Array(item["refs"] || item[:refs]).filter_map { |ref| circuit.net_of(ref, state)&.name }
|
|
71
|
+
end.uniq
|
|
72
|
+
outside_nets = circuit.nets(state).filter_map do |net|
|
|
73
|
+
next if declared_names.include?(net.name)
|
|
74
|
+
pins = net.members.select { |member| circuit.components.key?(member.split(".", 2).first) }.sort
|
|
75
|
+
next if pins.empty?
|
|
76
|
+
[net, pins]
|
|
77
|
+
end
|
|
78
|
+
outside_nets.sort_by { |_net, pins| pins.first }.each do |net, pins|
|
|
79
|
+
message = translate("undeclared_pins", "undeclared pins outside expected nets: #{pins.join(', ')}", pins: pins.join(", "))
|
|
80
|
+
first_net_entry = entries.find { |item| (item["kind"] || item[:kind]) == "net" }
|
|
81
|
+
location = first_net_entry && (first_net_entry["location"] || first_net_entry[:location])
|
|
82
|
+
results << offense(rule.id, message, location_from(location || expectation["location"] || expectation[:location]),
|
|
83
|
+
targets: { nets: [net.name], components: pins.map { |pin| pin.split(".", 2).first }.uniq,
|
|
84
|
+
pins: pins }, state: state.name)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
results
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def measured_expectation(circuit, rule, state, item)
|
|
92
|
+
return unless %w[Intent/ConnectionMismatch Intent/MeasurementUnavailable].include?(rule.id)
|
|
93
|
+
|
|
94
|
+
kind = item["kind"] || item[:kind]
|
|
95
|
+
reference = Array(item["refs"] || item[:refs]).first.to_s
|
|
96
|
+
range = item["range"] || item[:range]
|
|
97
|
+
location = location_from(item["location"] || item[:location])
|
|
98
|
+
analysis = circuit.dc_analysis(state)
|
|
99
|
+
if analysis.success?
|
|
100
|
+
net = circuit.net_of(reference, state) if kind == "voltage"
|
|
101
|
+
value = kind == "voltage" ? analysis.voltages[net&.name] : analysis.currents[reference]&.abs
|
|
102
|
+
floating = kind == "voltage" && analysis.floating.any? { |group| group.include?(net&.name) }
|
|
103
|
+
reason = "no grounded DC value" if value.nil? || floating
|
|
104
|
+
else
|
|
105
|
+
reason = analysis.errors.join("; ")
|
|
54
106
|
end
|
|
107
|
+
if reason
|
|
108
|
+
return unless rule.id == "Intent/MeasurementUnavailable"
|
|
109
|
+
|
|
110
|
+
message = translate("measurement_unavailable", "cannot verify #{kind} at #{reference}: #{reason}",
|
|
111
|
+
kind: kind, ref: reference, reason: reason)
|
|
112
|
+
return offense(rule.id, message, location, state: state.name)
|
|
113
|
+
end
|
|
114
|
+
return unless rule.id == "Intent/ConnectionMismatch" && (value < range[0] - 1e-9 || value > range[1] + 1e-9)
|
|
115
|
+
|
|
116
|
+
unit = kind == "voltage" ? "V" : "A"
|
|
117
|
+
message = translate("measurement_mismatch", "expected #{reference} #{kind} #{range.join('..')} #{unit}, found #{format('%.6g', value)} #{unit}",
|
|
118
|
+
ref: reference, kind: kind, range: range.join(".."), value: format("%.6g", value), unit: unit)
|
|
119
|
+
offense(rule.id, message, location, state: state.name)
|
|
55
120
|
end
|
|
56
121
|
|
|
57
122
|
end
|
|
@@ -28,6 +28,154 @@ module Breadkit
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def hole_covered(circuit, rule, _state)
|
|
32
|
+
circuit.components.values.flat_map do |owner|
|
|
33
|
+
bounds = body_rectangle(owner, circuit.board)
|
|
34
|
+
next [] unless bounds
|
|
35
|
+
left, top, width, height = bounds
|
|
36
|
+
inside = lambda do |hole|
|
|
37
|
+
hole && hole.x > left + 1e-6 && hole.x < left + width - 1e-6 &&
|
|
38
|
+
hole.y > top + 1e-6 && hole.y < top + height - 1e-6
|
|
39
|
+
end
|
|
40
|
+
leads = circuit.components.values.reject { |component| component.equal?(owner) }.flat_map do |component|
|
|
41
|
+
component.pins.values.filter_map do |pin|
|
|
42
|
+
hole = pin.hole_id && circuit.board.hole(pin.hole_id)
|
|
43
|
+
next unless inside.call(hole)
|
|
44
|
+
reference = "#{component.ref}.#{pin.name}"
|
|
45
|
+
offense(rule.id, translate("hole_covered_lead", "#{reference} is under #{owner.ref} at #{hole.id}",
|
|
46
|
+
pin: reference, module: owner.ref, hole: hole.id), component.location,
|
|
47
|
+
targets: { components: [owner.ref, component.ref], pins: [reference], holes: [hole.id] })
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
wires = circuit.wires.flat_map do |wire|
|
|
51
|
+
next [] if wire.electrical == false
|
|
52
|
+
[wire.from, wire.to].uniq.filter_map do |endpoint|
|
|
53
|
+
hole = circuit.board.hole(endpoint)
|
|
54
|
+
next unless inside.call(hole)
|
|
55
|
+
offense(rule.id, translate("hole_covered_wire", "#{wire.id} ends under #{owner.ref} at #{hole.id}",
|
|
56
|
+
wire: wire.id, module: owner.ref, hole: hole.id), wire.location,
|
|
57
|
+
targets: { components: [owner.ref], wires: [wire.id], holes: [hole.id] })
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
leads + wires
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def body_overlap(circuit, rule, _state)
|
|
65
|
+
bodies = circuit.components.values.filter_map do |component|
|
|
66
|
+
body = physical_body(component, circuit.board)
|
|
67
|
+
[component, body] if body
|
|
68
|
+
end
|
|
69
|
+
bodies.combination(2).filter_map do |(first, a), (second, b)|
|
|
70
|
+
next unless bodies_overlap?(a, b)
|
|
71
|
+
offense(rule.id, translate("body_overlap", "#{first.ref} and #{second.ref} bodies overlap",
|
|
72
|
+
first: first.ref, second: second.ref), second.location,
|
|
73
|
+
targets: { components: [first.ref, second.ref] })
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def wire_over_ic(circuit, rule, _state)
|
|
78
|
+
circuit.components.values.flat_map do |component|
|
|
79
|
+
bounds = dip_body_bounds(component, circuit.board)
|
|
80
|
+
next [] unless bounds
|
|
81
|
+
circuit.wires.filter_map do |wire|
|
|
82
|
+
next unless wire.electrical != false && wire.route == "straight"
|
|
83
|
+
from = circuit.board.hole(wire.from)
|
|
84
|
+
to = circuit.board.hole(wire.to)
|
|
85
|
+
next unless from && to
|
|
86
|
+
next unless segment_crosses_rect?(from, to, bounds)
|
|
87
|
+
offense(rule.id, translate("wire_over_ic", "#{wire.id} crosses #{component.ref} body",
|
|
88
|
+
wire: wire.id, ic: component.ref), wire.location,
|
|
89
|
+
targets: { components: [component.ref], wires: [wire.id] })
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def lead_span(circuit, rule, _state)
|
|
95
|
+
circuit.components.values.filter_map do |component|
|
|
96
|
+
part = component.part
|
|
97
|
+
next unless part.respond_to?(:max_lead_span_mm)
|
|
98
|
+
limit = part.max_lead_span_mm
|
|
99
|
+
next unless limit
|
|
100
|
+
pins = component.pins.values
|
|
101
|
+
next unless pins.length == 2
|
|
102
|
+
holes = pins.map { |pin| pin.hole_id && circuit.board.hole(pin.hole_id) }
|
|
103
|
+
next unless holes.all?
|
|
104
|
+
actual = Math.hypot(holes[0].x - holes[1].x, holes[0].y - holes[1].y) * 2.54
|
|
105
|
+
next unless actual > limit + 1e-6
|
|
106
|
+
actual_text, limit_text = format("%.3f", actual), format("%.3f", limit)
|
|
107
|
+
offense(rule.id, translate("lead_span", "#{component.ref} spans #{actual_text} mm; limit is #{limit_text} mm",
|
|
108
|
+
ref: component.ref, actual: actual_text, limit: limit_text),
|
|
109
|
+
component.location,
|
|
110
|
+
targets: { components: [component.ref], pins: pins.map { |pin| "#{component.ref}.#{pin.name}" }, holes: holes.map(&:id) })
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def dip_body_bounds(component, board)
|
|
115
|
+
return unless component.part.placement == "dip" && component.part.data.dig("render", "shape") == "dip"
|
|
116
|
+
holes = component.pins.values.filter_map { |pin| board.hole(pin.hole_id) if pin.hole_id }
|
|
117
|
+
return unless holes.length == component.part.pins.length && holes.all? { |hole| hole.kind == :terminal }
|
|
118
|
+
xs, ys = holes.map(&:x), holes.map(&:y).uniq.sort
|
|
119
|
+
return unless ys.length == 2 && (ys.last - ys.first - 3).abs < 1e-6
|
|
120
|
+
rows = holes.group_by(&:y).values.map { |row| row.map(&:x).sort }
|
|
121
|
+
return unless rows[0] == rows[1] && rows[0].length * 2 == holes.length
|
|
122
|
+
[xs.min - 0.4, (ys.first + ys.last) / 2.0 - 0.4, xs.max - xs.min + 0.8, 0.8]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def segment_crosses_rect?(from, to, bounds)
|
|
126
|
+
left, top, width, height = bounds
|
|
127
|
+
ranges = [[left + 1e-6, left + width - 1e-6, from.x, to.x],
|
|
128
|
+
[top + 1e-6, top + height - 1e-6, from.y, to.y]]
|
|
129
|
+
entry, exit = 0.0, 1.0
|
|
130
|
+
ranges.each do |minimum, maximum, start, finish|
|
|
131
|
+
delta = finish - start
|
|
132
|
+
return false if delta.abs < 1e-9 && !(start > minimum && start < maximum)
|
|
133
|
+
next if delta.abs < 1e-9
|
|
134
|
+
first, last = [(minimum - start) / delta, (maximum - start) / delta].minmax
|
|
135
|
+
entry = [entry, first].max
|
|
136
|
+
exit = [exit, last].min
|
|
137
|
+
end
|
|
138
|
+
entry < exit - 1e-9
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def physical_body(component, board)
|
|
142
|
+
shape = component.part.data.dig("render", "shape")
|
|
143
|
+
if %w[module dip].include?(shape)
|
|
144
|
+
bounds = body_rectangle(component, board)
|
|
145
|
+
return [:rect, *bounds] if bounds
|
|
146
|
+
elsif %w[led_5mm rgb_led_5mm].include?(shape)
|
|
147
|
+
holes = component.pins.values.filter_map { |pin| board.hole(pin.hole_id) if pin.hole_id }
|
|
148
|
+
return unless holes.length == component.pins.length && !holes.empty?
|
|
149
|
+
return [:circle, holes.sum(&:x) / holes.length, holes.sum(&:y) / holes.length, 2.5 / 2.54]
|
|
150
|
+
end
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def body_rectangle(component, board)
|
|
155
|
+
shape = component.part.data.dig("render", "shape")
|
|
156
|
+
return component.body_bounds(board) if shape == "module" && component.respond_to?(:body_bounds)
|
|
157
|
+
dip_body_bounds(component, board) if shape == "dip"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def bodies_overlap?(a, b)
|
|
161
|
+
if a.first == :rect && b.first == :rect
|
|
162
|
+
_kind, ax, ay, aw, ah = a
|
|
163
|
+
_kind, bx, by, bw, bh = b
|
|
164
|
+
return ax < bx + bw - 1e-6 && bx < ax + aw - 1e-6 &&
|
|
165
|
+
ay < by + bh - 1e-6 && by < ay + ah - 1e-6
|
|
166
|
+
end
|
|
167
|
+
if a.first == :circle && b.first == :circle
|
|
168
|
+
dx, dy = a[1] - b[1], a[2] - b[2]
|
|
169
|
+
return dx * dx + dy * dy < (a[3] + b[3] - 1e-6)**2
|
|
170
|
+
end
|
|
171
|
+
circle, rect = a.first == :circle ? [a, b] : [b, a]
|
|
172
|
+
_kind, cx, cy, radius = circle
|
|
173
|
+
_kind, rx, ry, width, height = rect
|
|
174
|
+
dx = cx - cx.clamp(rx, rx + width)
|
|
175
|
+
dy = cy - cy.clamp(ry, ry + height)
|
|
176
|
+
dx * dx + dy * dy < (radius - 1e-6)**2
|
|
177
|
+
end
|
|
178
|
+
|
|
31
179
|
end
|
|
32
180
|
end
|
|
33
181
|
end
|
|
@@ -10,7 +10,8 @@ module Breadkit
|
|
|
10
10
|
net = circuit.nets.find { |item| item.members.include?(wire.id) }
|
|
11
11
|
voltage = net && circuit.potentials.values[net.name]
|
|
12
12
|
expected = voltage&.positive? ? positive : (voltage == 0.0 ? ground : nil)
|
|
13
|
-
|
|
13
|
+
family = color_family(wire.color) if wire.color
|
|
14
|
+
next unless expected && family && !expected.include?(family)
|
|
14
15
|
message = translate("wire_color", "#{wire.id} uses #{wire.color}; expected #{expected.join(' or ')}",
|
|
15
16
|
wire: wire.id, color: wire.color, expected: expected.join(" / "))
|
|
16
17
|
offense(rule.id, message, wire.location,
|
|
@@ -20,10 +21,36 @@ module Breadkit
|
|
|
20
21
|
|
|
21
22
|
def color_family(color)
|
|
22
23
|
value = color.to_s.downcase
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return "gray" if value.match?(/gr[ae]y|silver|white/)
|
|
25
|
+
return "black" if value == "black"
|
|
26
|
+
return "red" if value.match?(/red|crimson|maroon|firebrick|coral|tomato/)
|
|
27
|
+
return "orange" if value.match?(/orange|gold|salmon/)
|
|
28
|
+
return "blue" if value.match?(/blue|navy|azure|cyan|teal|turquoise/)
|
|
29
|
+
return "green" if value.match?(/green|lime|chartreuse|olive|spring|aquamarine/)
|
|
30
|
+
return "yellow" if value.match?(/yellow|khaki/)
|
|
31
|
+
return "purple" if value.match?(/purple|violet|indigo|orchid|plum|magenta|fuchsia/)
|
|
32
|
+
return "pink" if value.match?(/pink|lavender/)
|
|
33
|
+
return "brown" if value.match?(/brown|tan|chocolate|peru|sienna|wheat/)
|
|
34
|
+
if (match = /\Ahsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)\z/.match(value))
|
|
35
|
+
hue, saturation, lightness = match.captures.map(&:to_i)
|
|
36
|
+
return "black" if lightness <= 15
|
|
37
|
+
return "gray" if saturation < 25
|
|
38
|
+
return "red" if hue < 15 || hue >= 345
|
|
39
|
+
return "orange" if hue < 50
|
|
40
|
+
return "yellow" if hue < 75
|
|
41
|
+
return "green" if hue < 165
|
|
42
|
+
return "blue" if hue.between?(165, 255)
|
|
43
|
+
return "purple"
|
|
44
|
+
end
|
|
45
|
+
if (match = /\Argb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\z/.match(value))
|
|
46
|
+
red, green, blue = match.captures.map(&:to_i)
|
|
47
|
+
elsif value.match?(/\A#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\z/)
|
|
48
|
+
digits = value.delete_prefix("#")
|
|
49
|
+
digits = digits.chars.map { |digit| digit * 2 }.join if digits.length <= 4
|
|
50
|
+
red, green, blue = digits.scan(/../).first(3).map { |pair| pair.to_i(16) }
|
|
51
|
+
else
|
|
52
|
+
return nil
|
|
53
|
+
end
|
|
27
54
|
max, min = [red, green, blue].max, [red, green, blue].min
|
|
28
55
|
return "black" if max <= 76
|
|
29
56
|
return "gray" if max == min || (max - min).fdiv(max) < 0.25
|
|
@@ -34,8 +61,10 @@ module Breadkit
|
|
|
34
61
|
end % 360
|
|
35
62
|
return "red" if hue < 15 || hue >= 345
|
|
36
63
|
return "orange" if hue < 50
|
|
37
|
-
return "
|
|
38
|
-
|
|
64
|
+
return "yellow" if hue < 75
|
|
65
|
+
return "green" if hue < 165
|
|
66
|
+
return "blue" if hue.between?(165, 255)
|
|
67
|
+
"purple"
|
|
39
68
|
end
|
|
40
69
|
|
|
41
70
|
end
|
|
@@ -22,18 +22,30 @@ module Breadkit
|
|
|
22
22
|
|
|
23
23
|
def diagnostic_targets(circuit, values)
|
|
24
24
|
ids = { components: circuit.components.keys, wires: circuit.wires.map(&:id),
|
|
25
|
-
holes: circuit.board.holes.keys,
|
|
25
|
+
holes: circuit.board.holes.keys,
|
|
26
|
+
pins: circuit.components.values.flat_map { |component| component.pins.values.map { |pin| "#{component.ref}.#{pin.name}" } },
|
|
27
|
+
nets: circuit.nets.map(&:name) }
|
|
26
28
|
Array(values).each_with_object({}) do |value, targets|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
canonical = canonical_pin(circuit, value)
|
|
30
|
+
kind = ids.find { |_type, known| known.include?(canonical) }&.first
|
|
31
|
+
(targets[kind] ||= []) << canonical if kind
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def diagnostic_message(diagnostic)
|
|
33
36
|
target = Array(diagnostic.targets).join(", ")
|
|
34
37
|
target = diagnostic.message[/unknown pin ([^;]+)/, 1] || target if diagnostic.code == "unknown_pin"
|
|
38
|
+
target = diagnostic.message[/unknown board: (.+)/, 1] || target if diagnostic.code == "unknown_board"
|
|
35
39
|
option = diagnostic.message[/unknown component option ([^ ]+)/, 1] if diagnostic.code == "unknown_option"
|
|
36
|
-
|
|
40
|
+
reason = case diagnostic.message
|
|
41
|
+
when /must straddle the center gap/ then translate("placement_straddle", "must straddle the center gap")
|
|
42
|
+
when /extends beyond the board or into the center gap/ then translate("placement_bounds_or_gap", "extends beyond the board or into the center gap")
|
|
43
|
+
when /extends beyond the board/ then translate("placement_bounds", "extends beyond the board")
|
|
44
|
+
when /pins do not match its footprint/ then translate("placement_footprint", "pins do not match the footprint")
|
|
45
|
+
when /needs a terminal hole anchor/ then translate("placement_anchor", "needs a terminal hole anchor")
|
|
46
|
+
else diagnostic.message
|
|
47
|
+
end
|
|
48
|
+
translate("diagnostic_#{diagnostic.code}", diagnostic.message, target: target, option: option, reason: reason)
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
def translate(key, english, **values)
|
|
@@ -41,35 +53,57 @@ module Breadkit
|
|
|
41
53
|
template ? format(template, **values) : english
|
|
42
54
|
end
|
|
43
55
|
|
|
44
|
-
def offense(rule_id, message, location, targets: {}, state: nil)
|
|
56
|
+
def offense(rule_id, message, location, targets: {}, state: nil, suggestion: nil)
|
|
45
57
|
rule = Registry.all.find { |item| item.id == rule_id }
|
|
46
58
|
Offense.new(rule: rule_id, severity: rule ? @config.severity(rule) : "error", message: message,
|
|
47
|
-
location: location, targets: targets, state: state)
|
|
59
|
+
location: location, targets: targets, state: state, suggestion: suggestion)
|
|
48
60
|
end
|
|
49
61
|
|
|
50
|
-
def suppress(offenses, disables, circuit)
|
|
62
|
+
def suppress(offenses, disables, circuit, path:, only: nil, except: nil, skipped: false)
|
|
51
63
|
invalid_disables = []
|
|
52
64
|
invalid = disables.filter_map do |disable|
|
|
53
65
|
rule_id = disable[:rule] || disable["rule"]
|
|
54
66
|
reason = disable[:reason] || disable["reason"]
|
|
55
|
-
|
|
67
|
+
unknown = Registry.all.none? { |rule| rule.id == rule_id }
|
|
68
|
+
message = if unknown
|
|
56
69
|
translate("unknown_disable", "unknown rule in lint_disable: #{rule_id}", rule: rule_id)
|
|
57
70
|
elsif @config.data.dig("AllRules", "RequireDisableReason") && reason.to_s.strip.empty?
|
|
58
71
|
translate("disable_reason", "lint_disable for #{rule_id} requires a reason", rule: rule_id)
|
|
59
72
|
end
|
|
60
73
|
next unless message
|
|
61
74
|
invalid_disables << disable
|
|
62
|
-
|
|
75
|
+
if unknown
|
|
76
|
+
rule = Rules::Lint::UnknownRuleInDisable
|
|
77
|
+
next if !@config.enabled?(rule) || !@config.rule_applies?(rule, path) || (only && !only.include?(rule.id)) || except&.include?(rule.id)
|
|
78
|
+
end
|
|
79
|
+
offense(unknown ? "Lint/UnknownRuleInDisable" : "Config/InvalidDisable", message,
|
|
80
|
+
location_from(disable[:location] || disable["location"]))
|
|
63
81
|
end
|
|
64
82
|
valid = disables - invalid_disables
|
|
83
|
+
used = []
|
|
65
84
|
kept = offenses.reject do |item|
|
|
66
|
-
valid.
|
|
85
|
+
match = valid.find do |disable|
|
|
67
86
|
rule = disable[:rule] || disable["rule"]
|
|
68
87
|
target = disable[:on] || disable["on"]
|
|
69
88
|
rule == item.rule && (!target || item.targets.values.flatten.include?(canonical_pin(circuit, target)))
|
|
70
89
|
end
|
|
90
|
+
used << match if match
|
|
91
|
+
!!match
|
|
92
|
+
end
|
|
93
|
+
redundant_selected = (!only || only.include?("Lint/RedundantDisable")) && !except&.include?("Lint/RedundantDisable")
|
|
94
|
+
redundant = valid.filter_map do |disable|
|
|
95
|
+
next unless redundant_selected
|
|
96
|
+
rule_id = disable[:rule] || disable["rule"]
|
|
97
|
+
rule = Registry.all.find { |item| item.id == rule_id }
|
|
98
|
+
next if used.include?(disable) || !rule || !@config.enabled?(rule) || !@config.rule_applies?(rule, path)
|
|
99
|
+
next if only && !only.include?(rule_id)
|
|
100
|
+
next if except&.include?(rule_id)
|
|
101
|
+
next if skipped && rule_id.start_with?("Electrical/", "Intent/")
|
|
102
|
+
next unless @config.enabled?(Rules::Lint::RedundantDisable) && @config.rule_applies?(Rules::Lint::RedundantDisable, path)
|
|
103
|
+
offense("Lint/RedundantDisable", translate("redundant_disable", "lint_disable for #{rule_id} suppresses no offense", rule: rule_id),
|
|
104
|
+
location_from(disable[:location] || disable["location"]))
|
|
71
105
|
end
|
|
72
|
-
kept + invalid
|
|
106
|
+
(kept + invalid + redundant).sort_by { |item| [item.location&.path.to_s, item.location&.line.to_i, item.rule, item.message] }
|
|
73
107
|
end
|
|
74
108
|
|
|
75
109
|
def canonical_pin(circuit, reference)
|