Class: DangerPackwerk::BasicReferenceOffense
- Inherits:
-
T::Struct
- Object
- T::Struct
- DangerPackwerk::BasicReferenceOffense
- Extended by:
- T::Sig
- Defined in:
- lib/danger-packwerk/basic_reference_offense.rb
Overview
We call this BasicReferenceOffense as it is intended to have a subset of the interface of Packwerk::ReferenceOffense, located here: github.com/Shopify/packwerk/blob/a22862b59f7760abf22bda6804d41a52d05301d8/lib/packwerk/reference_offense.rb#L1 However, we cannot actually construct a Packwerk::ReferenceOffense from ‘package_todo.yml` alone, since they are normally constructed in packwerk when packwerk parses the AST and actually outputs `package_todo.yml`, a process in which some information, such as the location where the constant is defined, is lost.
Defined Under Namespace
Classes: Location
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Object
- #dependency? ⇒ Boolean
- #eql?(other) ⇒ Boolean
- #hash ⇒ Object
- #privacy? ⇒ Boolean
Class Method Details
.from(package_todo_yml) ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/danger-packwerk/basic_reference_offense.rb', line 45 def self.from(package_todo_yml) package_todo_yml_pathname = Pathname.new(package_todo_yml) from_package = ParsePackwerk.package_from_path(package_todo_yml_pathname) from_package_name = from_package.name violations = ParsePackwerk::PackageTodo.from(package_todo_yml_pathname).violations # See the larger comment below for more information on why we need this information. # This is a small optimization that lets us find the location of referenced files within # a `package_todo.yml` file. Getting this now allows us to avoid reading through the file # once for every referenced file in the inner loop below. file_reference_to_line_number_index = T.let({}, T::Hash[String, T::Array[Integer]]) all_referenced_files = violations.flat_map(&:files).uniq package_todo_yml_pathname.readlines.each_with_index do |line, index| # We can use `find` here to exit early since each line will include one path that is unique to that file. # Paths should not be substrings of each other, since they are all paths relative to the root. file_on_line = all_referenced_files.find { |file| line.include?(file) } # Not all lines contain a reference to a file if file_on_line file_reference_to_line_number_index[file_on_line] ||= [] file_reference_to_line_number_index.fetch(file_on_line) << index end end violations.flat_map do |violation| # # We identify two locations associated with this violation. # First, we find the reference to the constant within the `package_todo.yml` file. # We know that each constant reference can occur only once per `package_todo.yml` file # The reason for this is that we know that only one file in the codebase can define a constant, and packwerk's constant_resolver will actually # raise if this assumption is not true: https://github.com/Shopify/constant_resolver/blob/e78af0c8d5782b06292c068cfe4176e016c51b34/lib/constant_resolver.rb#L74 # # Second, we find the reference to the specific file that references the constant within the `package_todo.yml` file. # This can occur multiple times per `package_todo.yml` file, but we know that the very first reference to the file after the class name key will be the one we care # about, so we take the first instance that occurs after the class is listed. # # Note though that since one constant reference in a `package_todo.yml` can be both a privacy and a dependency violation AND it can occur in many files, # we need to group them. That is -- if `MyPrivateConstant` is both a dependency and a privacy violation AND it occurs in 10 files, that would represent 20 violations. # Therefore we will group all of those 20 into one message to the user rather than providing 20 messages. # _line, class_name_line_number = package_todo_yml_pathname.readlines.each_with_index.find do |line, _index| # If you have a class `::MyClass`, then you can get a false match if another constant in the file # is named `MyOtherClass::MyClassThing`. Therefore we include quotes in our match to ensure that we match # the constant and only the constant. # Right now `packwerk` `package_todo.yml` files typically use double quotes, but sometimes folks linters change this to single quotes. # To be defensive, we match against either. patterns = [ /["|']#{violation.class_name}["|']:/, /\? ["|']#{violation.class_name}["|']$/ # for keys that use explicit mapping syntax ] patterns.any? { |p| line.match?(p) } end if class_name_line_number.nil? debug_info = { class_name: violation.class_name, to_package_name: violation.to_package_name, type: violation.type } raise "Unable to find reference to violation #{debug_info} in #{package_todo_yml}" end violation.files.map do |file| file_line_numbers = file_reference_to_line_number_index.fetch(file, []) file_line_number = file_line_numbers.select { |index| index > class_name_line_number }.min raise "Unable to find reference to violation #{{ file: file, to_package_name: violation.to_package_name, type: violation.type }} in #{package_todo_yml}" if file_line_number.nil? # We add one to the line number since `each_with_index` is zero-based indexed but Github line numbers are one-based indexed file_location = Location.new(file: package_todo_yml, line_number: file_line_number + 1) BasicReferenceOffense.new( class_name: violation.class_name, file: file, to_package_name: violation.to_package_name, type: violation.type, file_location: file_location, from_package_name: from_package_name ) end end end |
Instance Method Details
#==(other) ⇒ Object
136 137 138 139 140 141 |
# File 'lib/danger-packwerk/basic_reference_offense.rb', line 136 def ==(other) other.class_name == class_name && other.file == file && other.to_package_name == to_package_name && other.type == type end |
#dependency? ⇒ Boolean
131 132 133 |
# File 'lib/danger-packwerk/basic_reference_offense.rb', line 131 def dependency? type == 'dependency' end |
#eql?(other) ⇒ Boolean
144 145 146 |
# File 'lib/danger-packwerk/basic_reference_offense.rb', line 144 def eql?(other) self == other end |
#hash ⇒ Object
149 150 151 |
# File 'lib/danger-packwerk/basic_reference_offense.rb', line 149 def hash [class_name, file, to_package_name, type].hash end |
#privacy? ⇒ Boolean
126 127 128 |
# File 'lib/danger-packwerk/basic_reference_offense.rb', line 126 def privacy? type == 'privacy' end |