Skip to content

Commit d11ea6d

Browse files
committed
Add PksOffense struct for pks JSON violations
Create a T::Struct that represents violations from pks JSON output with fields for violation_type, file, line, column, constant_name, and pack names. Includes methods for JSON deserialization and compatibility with the BasicReferenceOffense interface used by existing formatters.
1 parent eb92983 commit d11ea6d

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed

lib/danger-packwerk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ module DangerPackwerk
1515
require 'danger-packwerk/check/default_formatter'
1616
require 'danger-packwerk/update/offenses_formatter'
1717
require 'danger-packwerk/update/default_formatter'
18+
require 'danger-packwerk/pks_offense'
1819
end

lib/danger-packwerk/pks_offense.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# typed: strict
2+
3+
require 'json'
4+
5+
module DangerPackwerk
6+
#
7+
# PksOffense represents a violation from pks JSON output.
8+
# It is designed to have a compatible interface with BasicReferenceOffense
9+
# so it can be used with the Update::OffensesFormatter.
10+
#
11+
class PksOffense < T::Struct
12+
extend T::Sig
13+
14+
const :violation_type, String
15+
const :file, String
16+
const :line, Integer
17+
const :column, Integer
18+
const :constant_name, String
19+
const :referencing_pack_name, String
20+
const :defining_pack_name, String
21+
const :strict, T::Boolean
22+
const :message, String
23+
24+
# Alias methods for compatibility with BasicReferenceOffense interface
25+
sig { returns(String) }
26+
def class_name
27+
constant_name
28+
end
29+
30+
sig { returns(String) }
31+
def type
32+
violation_type
33+
end
34+
35+
sig { returns(String) }
36+
def to_package_name
37+
defining_pack_name
38+
end
39+
40+
sig { returns(String) }
41+
def from_package_name
42+
referencing_pack_name
43+
end
44+
45+
sig { returns(T::Boolean) }
46+
def privacy?
47+
violation_type == PRIVACY_VIOLATION_TYPE
48+
end
49+
50+
sig { returns(T::Boolean) }
51+
def dependency?
52+
violation_type == DEPENDENCY_VIOLATION_TYPE
53+
end
54+
55+
sig { params(other: PksOffense).returns(T::Boolean) }
56+
def ==(other)
57+
other.constant_name == constant_name &&
58+
other.file == file &&
59+
other.defining_pack_name == defining_pack_name &&
60+
other.violation_type == violation_type
61+
end
62+
63+
sig { params(other: PksOffense).returns(T::Boolean) }
64+
def eql?(other)
65+
self == other
66+
end
67+
68+
sig { returns(Integer) }
69+
def hash
70+
[constant_name, file, defining_pack_name, violation_type].hash
71+
end
72+
73+
class << self
74+
extend T::Sig
75+
76+
sig { params(json_string: String).returns(T::Array[PksOffense]) }
77+
def from_json(json_string)
78+
data = JSON.parse(json_string)
79+
offenses = data['offenses'] || []
80+
offenses.map { |offense| from_hash(offense) }
81+
end
82+
83+
sig { params(hash: T::Hash[String, T.untyped]).returns(PksOffense) }
84+
def from_hash(hash)
85+
PksOffense.new(
86+
violation_type: hash.fetch('violation_type'),
87+
file: hash.fetch('file'),
88+
line: hash.fetch('line'),
89+
column: hash.fetch('column'),
90+
constant_name: hash.fetch('constant_name'),
91+
referencing_pack_name: hash.fetch('referencing_pack_name'),
92+
defining_pack_name: hash.fetch('defining_pack_name'),
93+
strict: hash.fetch('strict', false),
94+
message: hash.fetch('message', '')
95+
)
96+
end
97+
end
98+
end
99+
end
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
require 'spec_helper'
2+
3+
module DangerPackwerk
4+
RSpec.describe PksOffense do
5+
# PksOffense doesn't need Danger plugin context, but spec_helper includes it
6+
let(:plugin) { dangerfile.packwerk }
7+
let(:privacy_offense_hash) do
8+
{
9+
'violation_type' => 'privacy',
10+
'file' => 'packs/my_pack/app/models/user.rb',
11+
'line' => 42,
12+
'column' => 10,
13+
'constant_name' => '::OtherPack::PrivateClass',
14+
'referencing_pack_name' => 'packs/my_pack',
15+
'defining_pack_name' => 'packs/other_pack',
16+
'strict' => false,
17+
'message' => 'Privacy violation: ::OtherPack::PrivateClass is private'
18+
}
19+
end
20+
21+
let(:dependency_offense_hash) do
22+
{
23+
'violation_type' => 'dependency',
24+
'file' => 'packs/my_pack/app/services/user_service.rb',
25+
'line' => 15,
26+
'column' => 5,
27+
'constant_name' => '::ThirdPack::SomeConstant',
28+
'referencing_pack_name' => 'packs/my_pack',
29+
'defining_pack_name' => 'packs/third_pack',
30+
'strict' => true,
31+
'message' => 'Dependency violation: packs/my_pack does not declare packs/third_pack as a dependency'
32+
}
33+
end
34+
35+
describe '.from_hash' do
36+
it 'creates a PksOffense from a hash' do
37+
offense = described_class.from_hash(privacy_offense_hash)
38+
39+
expect(offense.violation_type).to eq('privacy')
40+
expect(offense.file).to eq('packs/my_pack/app/models/user.rb')
41+
expect(offense.line).to eq(42)
42+
expect(offense.column).to eq(10)
43+
expect(offense.constant_name).to eq('::OtherPack::PrivateClass')
44+
expect(offense.referencing_pack_name).to eq('packs/my_pack')
45+
expect(offense.defining_pack_name).to eq('packs/other_pack')
46+
expect(offense.strict).to eq(false)
47+
expect(offense.message).to eq('Privacy violation: ::OtherPack::PrivateClass is private')
48+
end
49+
50+
it 'defaults strict to false when not provided' do
51+
hash = privacy_offense_hash.except('strict')
52+
offense = described_class.from_hash(hash)
53+
expect(offense.strict).to eq(false)
54+
end
55+
56+
it 'defaults message to empty string when not provided' do
57+
hash = privacy_offense_hash.except('message')
58+
offense = described_class.from_hash(hash)
59+
expect(offense.message).to eq('')
60+
end
61+
end
62+
63+
describe '.from_json' do
64+
it 'parses JSON string with offenses array' do
65+
json = {
66+
'offenses' => [privacy_offense_hash, dependency_offense_hash]
67+
}.to_json
68+
69+
offenses = described_class.from_json(json)
70+
71+
expect(offenses.length).to eq(2)
72+
expect(offenses[0].violation_type).to eq('privacy')
73+
expect(offenses[1].violation_type).to eq('dependency')
74+
end
75+
76+
it 'returns empty array when no offenses key' do
77+
json = {}.to_json
78+
offenses = described_class.from_json(json)
79+
expect(offenses).to eq([])
80+
end
81+
82+
it 'returns empty array when offenses is empty' do
83+
json = { 'offenses' => [] }.to_json
84+
offenses = described_class.from_json(json)
85+
expect(offenses).to eq([])
86+
end
87+
end
88+
89+
describe 'BasicReferenceOffense interface compatibility' do
90+
let(:offense) { described_class.from_hash(privacy_offense_hash) }
91+
92+
it 'provides class_name alias for constant_name' do
93+
expect(offense.class_name).to eq(offense.constant_name)
94+
end
95+
96+
it 'provides type alias for violation_type' do
97+
expect(offense.type).to eq(offense.violation_type)
98+
end
99+
100+
it 'provides to_package_name alias for defining_pack_name' do
101+
expect(offense.to_package_name).to eq(offense.defining_pack_name)
102+
end
103+
104+
it 'provides from_package_name alias for referencing_pack_name' do
105+
expect(offense.from_package_name).to eq(offense.referencing_pack_name)
106+
end
107+
end
108+
109+
describe '#privacy?' do
110+
it 'returns true for privacy violations' do
111+
offense = described_class.from_hash(privacy_offense_hash)
112+
expect(offense.privacy?).to eq(true)
113+
expect(offense.dependency?).to eq(false)
114+
end
115+
end
116+
117+
describe '#dependency?' do
118+
it 'returns true for dependency violations' do
119+
offense = described_class.from_hash(dependency_offense_hash)
120+
expect(offense.dependency?).to eq(true)
121+
expect(offense.privacy?).to eq(false)
122+
end
123+
end
124+
125+
describe 'equality' do
126+
let(:offense1) { described_class.from_hash(privacy_offense_hash) }
127+
let(:offense2) { described_class.from_hash(privacy_offense_hash) }
128+
let(:different_offense) { described_class.from_hash(dependency_offense_hash) }
129+
130+
it 'considers offenses equal when key fields match' do
131+
expect(offense1).to eq(offense2)
132+
expect(offense1.eql?(offense2)).to eq(true)
133+
end
134+
135+
it 'considers offenses different when key fields differ' do
136+
expect(offense1).not_to eq(different_offense)
137+
expect(offense1.eql?(different_offense)).to eq(false)
138+
end
139+
140+
it 'produces consistent hash values for equal offenses' do
141+
expect(offense1.hash).to eq(offense2.hash)
142+
end
143+
144+
it 'can be used in a Set' do
145+
set = Set.new([offense1, offense2, different_offense])
146+
expect(set.length).to eq(2)
147+
end
148+
149+
it 'can be used as hash keys' do
150+
hash = { offense1 => 'first', offense2 => 'second' }
151+
expect(hash.length).to eq(1)
152+
expect(hash[offense1]).to eq('second')
153+
end
154+
end
155+
end
156+
end

0 commit comments

Comments
 (0)