codeclimate-fede 0.85.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/bin/check +18 -0
  3. data/bin/codeclimate +21 -0
  4. data/bin/prep-release +45 -0
  5. data/bin/release +41 -0
  6. data/bin/validate-release +18 -0
  7. data/config/engines.yml +322 -0
  8. data/lib/cc/analyzer.rb +50 -0
  9. data/lib/cc/analyzer/bridge.rb +106 -0
  10. data/lib/cc/analyzer/composite_container_listener.rb +21 -0
  11. data/lib/cc/analyzer/container.rb +208 -0
  12. data/lib/cc/analyzer/container/result.rb +74 -0
  13. data/lib/cc/analyzer/container_listener.rb +9 -0
  14. data/lib/cc/analyzer/engine.rb +125 -0
  15. data/lib/cc/analyzer/engine_output.rb +74 -0
  16. data/lib/cc/analyzer/engine_output_filter.rb +36 -0
  17. data/lib/cc/analyzer/engine_output_overrider.rb +31 -0
  18. data/lib/cc/analyzer/filesystem.rb +50 -0
  19. data/lib/cc/analyzer/formatters.rb +21 -0
  20. data/lib/cc/analyzer/formatters/formatter.rb +53 -0
  21. data/lib/cc/analyzer/formatters/html_formatter.rb +415 -0
  22. data/lib/cc/analyzer/formatters/json_formatter.rb +38 -0
  23. data/lib/cc/analyzer/formatters/plain_text_formatter.rb +101 -0
  24. data/lib/cc/analyzer/formatters/spinner.rb +35 -0
  25. data/lib/cc/analyzer/issue.rb +69 -0
  26. data/lib/cc/analyzer/issue_sorter.rb +30 -0
  27. data/lib/cc/analyzer/issue_validations.rb +26 -0
  28. data/lib/cc/analyzer/issue_validations/category_validation.rb +32 -0
  29. data/lib/cc/analyzer/issue_validations/check_name_presence_validation.rb +15 -0
  30. data/lib/cc/analyzer/issue_validations/content_validation.rb +21 -0
  31. data/lib/cc/analyzer/issue_validations/description_presence_validation.rb +15 -0
  32. data/lib/cc/analyzer/issue_validations/location_format_validation.rb +72 -0
  33. data/lib/cc/analyzer/issue_validations/other_locations_format_validation.rb +41 -0
  34. data/lib/cc/analyzer/issue_validations/path_existence_validation.rb +15 -0
  35. data/lib/cc/analyzer/issue_validations/path_is_file_validation.rb +15 -0
  36. data/lib/cc/analyzer/issue_validations/path_presence_validation.rb +15 -0
  37. data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +32 -0
  38. data/lib/cc/analyzer/issue_validations/remediation_points_validation.rb +25 -0
  39. data/lib/cc/analyzer/issue_validations/severity_validation.rb +39 -0
  40. data/lib/cc/analyzer/issue_validations/type_validation.rb +15 -0
  41. data/lib/cc/analyzer/issue_validations/validation.rb +35 -0
  42. data/lib/cc/analyzer/issue_validator.rb +11 -0
  43. data/lib/cc/analyzer/location_description.rb +45 -0
  44. data/lib/cc/analyzer/logging_container_listener.rb +24 -0
  45. data/lib/cc/analyzer/measurement.rb +22 -0
  46. data/lib/cc/analyzer/measurement_validations.rb +16 -0
  47. data/lib/cc/analyzer/measurement_validations/name_validation.rb +23 -0
  48. data/lib/cc/analyzer/measurement_validations/type_validation.rb +15 -0
  49. data/lib/cc/analyzer/measurement_validations/validation.rb +27 -0
  50. data/lib/cc/analyzer/measurement_validations/value_validation.rb +21 -0
  51. data/lib/cc/analyzer/measurement_validator.rb +11 -0
  52. data/lib/cc/analyzer/mounted_path.rb +80 -0
  53. data/lib/cc/analyzer/raising_container_listener.rb +32 -0
  54. data/lib/cc/analyzer/source_buffer.rb +47 -0
  55. data/lib/cc/analyzer/source_extractor.rb +79 -0
  56. data/lib/cc/analyzer/source_fingerprint.rb +40 -0
  57. data/lib/cc/analyzer/statsd_container_listener.rb +51 -0
  58. data/lib/cc/analyzer/validator.rb +38 -0
  59. data/lib/cc/cli.rb +39 -0
  60. data/lib/cc/cli/analyze.rb +90 -0
  61. data/lib/cc/cli/analyze/engine_failure.rb +11 -0
  62. data/lib/cc/cli/command.rb +85 -0
  63. data/lib/cc/cli/console.rb +12 -0
  64. data/lib/cc/cli/engines.rb +5 -0
  65. data/lib/cc/cli/engines/engine_command.rb +15 -0
  66. data/lib/cc/cli/engines/install.rb +35 -0
  67. data/lib/cc/cli/engines/list.rb +18 -0
  68. data/lib/cc/cli/file_store.rb +42 -0
  69. data/lib/cc/cli/global_cache.rb +47 -0
  70. data/lib/cc/cli/global_config.rb +35 -0
  71. data/lib/cc/cli/help.rb +51 -0
  72. data/lib/cc/cli/output.rb +34 -0
  73. data/lib/cc/cli/prepare.rb +98 -0
  74. data/lib/cc/cli/runner.rb +75 -0
  75. data/lib/cc/cli/validate_config.rb +84 -0
  76. data/lib/cc/cli/version.rb +16 -0
  77. data/lib/cc/cli/version_checker.rb +107 -0
  78. data/lib/cc/config.rb +70 -0
  79. data/lib/cc/config/checks_adapter.rb +40 -0
  80. data/lib/cc/config/default_adapter.rb +54 -0
  81. data/lib/cc/config/engine.rb +41 -0
  82. data/lib/cc/config/engine_set.rb +47 -0
  83. data/lib/cc/config/json_adapter.rb +17 -0
  84. data/lib/cc/config/prepare.rb +92 -0
  85. data/lib/cc/config/validation/check_validator.rb +34 -0
  86. data/lib/cc/config/validation/engine_validator.rb +93 -0
  87. data/lib/cc/config/validation/fetch_validator.rb +78 -0
  88. data/lib/cc/config/validation/file_validator.rb +112 -0
  89. data/lib/cc/config/validation/hash_validations.rb +52 -0
  90. data/lib/cc/config/validation/json.rb +31 -0
  91. data/lib/cc/config/validation/prepare_validator.rb +40 -0
  92. data/lib/cc/config/validation/yaml.rb +66 -0
  93. data/lib/cc/config/yaml_adapter.rb +73 -0
  94. data/lib/cc/engine_registry.rb +74 -0
  95. data/lib/cc/resolv.rb +39 -0
  96. data/lib/cc/workspace.rb +39 -0
  97. data/lib/cc/workspace/exclusion.rb +34 -0
  98. data/lib/cc/workspace/path_tree.rb +49 -0
  99. data/lib/cc/workspace/path_tree/dir_node.rb +67 -0
  100. data/lib/cc/workspace/path_tree/file_node.rb +31 -0
  101. metadata +277 -0
@@ -0,0 +1,9 @@
1
+ module CC
2
+ module Analyzer
3
+ class ContainerListener
4
+ def started(_engine, _details); end
5
+
6
+ def finished(_engine, _details, _result); end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,125 @@
1
+ require "securerandom"
2
+
3
+ module CC
4
+ module Analyzer
5
+ #
6
+ # Running specifically an Engine container
7
+ #
8
+ # Input:
9
+ # - name
10
+ # - metadata
11
+ # - image
12
+ # - command (optional)
13
+ # - config (becomes /config.json)
14
+ # - label
15
+ # - io (to write filtered, validated output)
16
+ #
17
+ # Output:
18
+ # - Container::Result
19
+ #
20
+ class Engine
21
+ Error = Class.new(StandardError)
22
+
23
+ def initialize(name, metadata, config, label)
24
+ @name = name
25
+ @metadata = metadata
26
+ @config = config
27
+ @label = label.to_s
28
+ @error = nil
29
+ end
30
+
31
+ def run(io)
32
+ write_config_file
33
+
34
+ container = Container.new(
35
+ image: metadata.fetch("image"),
36
+ command: metadata["command"],
37
+ name: container_name,
38
+ )
39
+
40
+ container.on_output("\0") do |output|
41
+ handle_output(container, io, output)
42
+ end
43
+
44
+ container.run(container_options).tap do |result|
45
+ result.merge_from_exception(error) if error.present?
46
+ end
47
+ ensure
48
+ delete_config_file
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :name, :metadata
54
+ attr_accessor :error
55
+
56
+ def handle_output(container, io, raw_output)
57
+ output = EngineOutput.new(name, raw_output)
58
+
59
+ return if output_filter.filter?(output)
60
+
61
+ unless output.valid?
62
+ self.error = Error.new("engine produced invalid output: #{output.error}")
63
+ container.stop("output invalid")
64
+ end
65
+
66
+ unless io.write(output_overrider.apply(output).to_json)
67
+ self.error = Error.new("#{io.class}#write returned false, indicating an error")
68
+ container.stop("output error")
69
+ end
70
+ end
71
+
72
+ def qualified_name
73
+ "#{name}:#{@config.fetch("channel", "stable")}"
74
+ end
75
+
76
+ def container_options
77
+ options = [
78
+ "--cap-drop", "all",
79
+ "--label", "com.codeclimate.label=#{@label}",
80
+ "--log-driver", "none",
81
+ "--memory-swap", "-1",
82
+ "--net", "none",
83
+ "--rm",
84
+ "--volume", "#{code.host_path}:/code:ro",
85
+ "--volume", "#{config_file.host_path}:/config.json:ro",
86
+ "--user", "9000:9000"
87
+ ]
88
+ if (memory = metadata["memory"]).present?
89
+ options.concat(["--memory", memory.to_s])
90
+ end
91
+ options
92
+ end
93
+
94
+ def container_name
95
+ @container_name ||= "cc-engines-#{qualified_name.tr(":", "-")}-#{SecureRandom.uuid}"
96
+ end
97
+
98
+ def write_config_file
99
+ @config["debug"] = ENV["CODECLIMATE_DEBUG"]
100
+ Analyzer.logger.debug "/config.json content: #{@config.inspect}"
101
+ config_file.write(@config.to_json)
102
+ end
103
+
104
+ def delete_config_file
105
+ config_file.delete if config_file.file?
106
+ end
107
+
108
+ def code
109
+ @code ||= MountedPath.code
110
+ end
111
+
112
+ def config_file
113
+ @config_file ||= MountedPath.tmp.join(SecureRandom.uuid)
114
+ end
115
+
116
+ def output_filter
117
+ @output_filter ||= EngineOutputFilter.new(@config)
118
+ end
119
+
120
+ def output_overrider
121
+ @output_overrider ||= EngineOutputOverrider.new(@config)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,74 @@
1
+ module CC
2
+ module Analyzer
3
+ class EngineOutput
4
+ delegate :blank?, to: :raw_output
5
+
6
+ def initialize(name, raw_output)
7
+ @name = name
8
+ @raw_output = raw_output
9
+ end
10
+
11
+ def issue?
12
+ valid_with_type?("issue")
13
+ end
14
+
15
+ def measurement?
16
+ valid_with_type?("measurement")
17
+ end
18
+
19
+ def as_issue
20
+ Issue.new(name, raw_output)
21
+ end
22
+
23
+ def to_json
24
+ if issue?
25
+ as_issue.to_json
26
+ elsif measurement?
27
+ Measurement.new(name, raw_output).to_json
28
+ end
29
+ end
30
+
31
+ def valid?
32
+ valid_json? && validator && validator.valid?
33
+ end
34
+
35
+ def error
36
+ if !valid_json?
37
+ { message: "Invalid JSON", output: raw_output }
38
+ elsif !validator.present?
39
+ { message: "Unsupported document type", output: raw_output }
40
+ else
41
+ validator.error
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ attr_accessor :name, :raw_output
48
+
49
+ def valid_json?
50
+ parsed_output.present?
51
+ end
52
+
53
+ def valid_with_type?(type)
54
+ parsed_output &&
55
+ parsed_output["type"].present? &&
56
+ parsed_output["type"].downcase == type
57
+ end
58
+
59
+ def parsed_output
60
+ @parsed_output ||= JSON.parse(raw_output)
61
+ rescue JSON::ParserError
62
+ nil
63
+ end
64
+
65
+ def validator
66
+ if issue?
67
+ IssueValidator.new(parsed_output)
68
+ elsif measurement?
69
+ MeasurementValidator.new(parsed_output)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,36 @@
1
+ module CC
2
+ module Analyzer
3
+ class EngineOutputFilter
4
+ ISSUE_TYPE = "issue".freeze
5
+
6
+ def initialize(config = {})
7
+ @config = config
8
+ end
9
+
10
+ def filter?(output)
11
+ output.blank? || (output.issue? && ignore_issue?(output.as_issue))
12
+ end
13
+
14
+ private
15
+
16
+ def ignore_issue?(issue)
17
+ check_disabled?(issue) || ignore_fingerprint?(issue)
18
+ end
19
+
20
+ def check_disabled?(issue)
21
+ !check_config(issue.check_name).fetch("enabled", true)
22
+ end
23
+
24
+ def ignore_fingerprint?(issue)
25
+ @config.fetch("exclude_fingerprints", []).include?(issue.fingerprint)
26
+ rescue SourceExtractor::InvalidLocation
27
+ false
28
+ end
29
+
30
+ def check_config(check_name)
31
+ @checks ||= @config.fetch("checks", {})
32
+ @checks.fetch(check_name, {})
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module CC
2
+ module Analyzer
3
+ class EngineOutputOverrider
4
+ def initialize(config = {})
5
+ @config = config
6
+ end
7
+
8
+ def apply(output)
9
+ if output.issue?
10
+ override_severity(output.as_issue.as_json)
11
+ else
12
+ output
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :config
19
+
20
+ def override_severity(issue)
21
+ issue.merge(override("severity"))
22
+ end
23
+
24
+ def override(name)
25
+ config.
26
+ fetch("issue_override", {}).
27
+ slice(name)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ module CC
2
+ module Analyzer
3
+ class Filesystem
4
+ attr_reader :root
5
+
6
+ def initialize(root)
7
+ @root = root
8
+ end
9
+
10
+ def exist?(path)
11
+ File.exist?(path_for(path))
12
+ end
13
+
14
+ def source_buffer_for(path)
15
+ SourceBuffer.new(path, read_path(path))
16
+ end
17
+
18
+ def read_path(path)
19
+ File.read(path_for(path))
20
+ end
21
+
22
+ def write_path(path, content)
23
+ File.write(path_for(path), content)
24
+ File.chown(root_uid, root_gid, path_for(path))
25
+ end
26
+
27
+ def ls
28
+ Dir.entries(root).reject { |entry| [".", ".."].include?(entry) }
29
+ end
30
+
31
+ private
32
+
33
+ def path_for(path)
34
+ File.join(root, path)
35
+ end
36
+
37
+ def root_uid
38
+ root_stat.uid
39
+ end
40
+
41
+ def root_gid
42
+ root_stat.gid
43
+ end
44
+
45
+ def root_stat
46
+ @root_stat ||= File.stat(root)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ module CC
2
+ module Analyzer
3
+ module Formatters
4
+ autoload :Formatter, "cc/analyzer/formatters/formatter"
5
+ autoload :HTMLFormatter, "cc/analyzer/formatters/html_formatter"
6
+ autoload :JSONFormatter, "cc/analyzer/formatters/json_formatter"
7
+ autoload :PlainTextFormatter, "cc/analyzer/formatters/plain_text_formatter"
8
+ autoload :Spinner, "cc/analyzer/formatters/spinner"
9
+
10
+ FORMATTERS = {
11
+ html: HTMLFormatter,
12
+ json: JSONFormatter,
13
+ text: PlainTextFormatter,
14
+ }.freeze
15
+
16
+ def self.resolve(name)
17
+ FORMATTERS[name.to_sym] or raise Formatter::InvalidFormatterError, "'#{name}' is not a valid formatter. Valid options are: #{FORMATTERS.keys.join(", ")}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ module CC
2
+ module Analyzer
3
+ module Formatters
4
+ class Formatter
5
+ def initialize(filesystem, output = $stdout)
6
+ @filesystem = filesystem
7
+ @output = output
8
+ end
9
+
10
+ def write(data)
11
+ json = JSON.parse(data)
12
+ json["engine_name"] = current_engine.name
13
+
14
+ case json["type"].downcase
15
+ when "issue"
16
+ issues << json
17
+ when "warning"
18
+ warnings << json
19
+ when "measurement"
20
+ measurements << json
21
+ else
22
+ raise "Invalid type found: #{json["type"]}"
23
+ end
24
+ end
25
+
26
+ def started
27
+ end
28
+
29
+ def engine_running(engine)
30
+ @current_engine = engine
31
+ yield
32
+ ensure
33
+ @current_engine = nil
34
+ end
35
+
36
+ def finished
37
+ end
38
+
39
+ def close
40
+ end
41
+
42
+ def failed(_output)
43
+ end
44
+
45
+ InvalidFormatterError = Class.new(StandardError)
46
+
47
+ private
48
+
49
+ attr_reader :current_engine
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,415 @@
1
+ require "redcarpet"
2
+
3
+ module CC
4
+ module Analyzer
5
+ module Formatters
6
+ class HTMLFormatter < Formatter # rubocop: disable Metrics/ClassLength
7
+ LANGUAGES = Hash.new { |_, ext| ext }.
8
+ merge(
9
+ # abap
10
+ # ada
11
+ "appraisals" => "ruby",
12
+ "as" => "actionscript",
13
+ "asm" => "nasm",
14
+ "bas" => "basic",
15
+ # c
16
+ "c++" => "cpp",
17
+ "capfile" => "ruby",
18
+ "cc" => "cpp",
19
+ "cfc" => "markup",
20
+ "cfm" => "markup",
21
+ "coffee" => "coffeescript",
22
+ "cp" => "cpp",
23
+ # cpp
24
+ "cr" => "crystal",
25
+ "cs" => "csharp",
26
+ "css" => %w[css css-extras],
27
+ "cu" => "cpp",
28
+ "cxx" => "cpp",
29
+ # d
30
+ # dart
31
+ # diff
32
+ "dockerfile" => "docker",
33
+ "dpr" => "pascal",
34
+ "erl" => "erlang",
35
+ "ex" => "elixir",
36
+ "f" => "fortran",
37
+ "f90" => "fortran",
38
+ "f95" => "fortran",
39
+ "feature" => "gherkin",
40
+ "for" => "fortran",
41
+ "fs" => "fsharp",
42
+ "fsi" => "fsharp",
43
+ "fsscript" => "fsharp",
44
+ "fsx" => "fsharp",
45
+ "gemfile" => "ruby",
46
+ "gemspec" => "ruby",
47
+ # glsl
48
+ # go
49
+ # groovy
50
+ "gvy" => "groovy",
51
+ "h" => "c",
52
+ "h++" => "cpp",
53
+ # haml
54
+ # handlebars
55
+ "hbr" => "handlebars",
56
+ "hh" => "cpp",
57
+ "hpp" => "cpp",
58
+ "hs" => "haskell",
59
+ "htm" => "markup",
60
+ "html" => "markup",
61
+ "hx" => "haxe",
62
+ "hxml" => "haxe",
63
+ "icn" => "icon",
64
+ "ijs" => "j",
65
+ # ini
66
+ "iol" => "jolie",
67
+ # java
68
+ "jl" => "julia",
69
+ "js" => "javascript",
70
+ # json
71
+ # jsx
72
+ "kt" => "kotlin",
73
+ "kts" => "kotlin",
74
+ # less
75
+ "lhs" => "haskell",
76
+ "lol" => "lolcode",
77
+ "lols" => "lolcode",
78
+ "ls" => "livescript",
79
+ # lua
80
+ "m" => "objective-c",
81
+ "mab" => "ruby",
82
+ # makefile
83
+ # markdown
84
+ "md" => "markdown",
85
+ # mel
86
+ "mkd" => "markdown",
87
+ "ml" => "ocaml",
88
+ "mli" => "ocaml",
89
+ "mm" => "objective-c",
90
+ # nim
91
+ # nix
92
+ "nsi" => "nsis",
93
+ "ol" => "jolie",
94
+ # oz
95
+ "pas" => "pascal",
96
+ "patch" => "diff",
97
+ "pde" => "processing",
98
+ "php" => %w[php php-extras],
99
+ "php3" => %w[php php-extras],
100
+ "php4" => %w[php php-extras],
101
+ "php5" => %w[php php-extras],
102
+ "phtml" => %w[php php-extras],
103
+ "pl" => "perl",
104
+ "pp" => "puppet",
105
+ "prawn" => "ruby",
106
+ "pro" => "prolog",
107
+ # properties
108
+ # pure
109
+ "py" => "python",
110
+ "py3" => "python",
111
+ "pyw" => "python",
112
+ "q" => "qore",
113
+ "qm" => "qore",
114
+ "qtest" => "qore",
115
+ # r
116
+ "rake" => "ruby",
117
+ "rakefile" => "ruby",
118
+ "rantfile" => "ruby",
119
+ "rb" => "ruby",
120
+ "rbw" => "ruby",
121
+ "rjs" => "ruby",
122
+ "rpdf" => "ruby",
123
+ "rs" => "rust",
124
+ "rst" => "rest",
125
+ "ru" => "ruby",
126
+ "rxml" => "ruby",
127
+ # sass
128
+ "sc" => "scala",
129
+ # scala
130
+ "scs" => "scheme",
131
+ # scss
132
+ "shader" => "glsl",
133
+ # sql
134
+ "ss" => "scheme",
135
+ "st" => "smalltalk",
136
+ "styl" => "stylus",
137
+ # swift
138
+ # tcl
139
+ "template" => "json",
140
+ "tex" => "latex",
141
+ # textile
142
+ "tmproj" => "markup",
143
+ "tpl" => "smarty",
144
+ "ts" => "typescript",
145
+ "v" => "verilog",
146
+ "vagrantfile" => "ruby",
147
+ "vhd" => "vhdl",
148
+ # vim
149
+ "xaml" => "markup",
150
+ "xhtml" => "markup",
151
+ "xml" => "markup",
152
+ # yaml
153
+ "yaws" => "erlang",
154
+ "yml" => "yaml",
155
+ ).freeze
156
+
157
+ class Location
158
+ CONTEXT_LINES = 2
159
+ MAX_LINES = 10
160
+
161
+ def initialize(source_buffer, location)
162
+ @source_buffer = source_buffer
163
+ @location = location
164
+ end
165
+
166
+ def begin_line
167
+ @begin_line ||= line("begin")
168
+ end
169
+
170
+ def end_line
171
+ @end_line ||= line("end")
172
+ end
173
+
174
+ def to_s
175
+ [
176
+ begin_line,
177
+ end_line,
178
+ ].uniq.join("-")
179
+ end
180
+
181
+ def start
182
+ [begin_line - CONTEXT_LINES, 1].max
183
+ end
184
+
185
+ def line_offset
186
+ start - 1
187
+ end
188
+
189
+ def code
190
+ first_line = start
191
+ last_line = [
192
+ end_line + CONTEXT_LINES,
193
+ begin_line + MAX_LINES + CONTEXT_LINES,
194
+ source_buffer.line_count,
195
+ ].min
196
+ source_buffer.source.lines[(first_line - 1)..(last_line - 1)].join("")
197
+ end
198
+
199
+ private
200
+
201
+ attr_reader :location, :source_buffer
202
+
203
+ def line(type)
204
+ if location["lines"]
205
+ location["lines"][type]
206
+ elsif location["positions"]
207
+ position_to_line(location["positions"][type])
208
+ end
209
+ end
210
+
211
+ def position_to_line(position)
212
+ if position["line"]
213
+ position["line"]
214
+ else
215
+ @source_buffer.decompose_position(position["offset"]).first
216
+ end
217
+ end
218
+ end
219
+
220
+ class SourceFile
221
+ def initialize(path, filesystem)
222
+ @path = path
223
+ @filesystem = filesystem
224
+ end
225
+
226
+ attr_reader :path
227
+
228
+ def syntaxes
229
+ ext = File.basename(path).split(".").last.downcase
230
+ Array(LANGUAGES[ext])
231
+ end
232
+
233
+ def code
234
+ filesystem.read_path(path)
235
+ end
236
+
237
+ def buffer
238
+ @buffer ||= SourceBuffer.new(path, code)
239
+ end
240
+
241
+ def location(loc)
242
+ Location.new(buffer, loc)
243
+ end
244
+
245
+ private
246
+
247
+ attr_reader :filesystem
248
+ end
249
+
250
+ class Issue
251
+ MARKDOWN_CONFIG = { autolink: true, fenced_code_blocks: true, no_intra_emphasis: true, tables: true }.freeze
252
+
253
+ def initialize(data, filesystem)
254
+ @data = data
255
+ @filesystem = filesystem
256
+ end
257
+
258
+ def description
259
+ data["description"]
260
+ end
261
+
262
+ def body
263
+ @body ||=
264
+ begin
265
+ text = data.fetch("content", {}).fetch("body", "").strip
266
+ unless text.empty?
267
+ markdown(text)
268
+ end
269
+ end
270
+ end
271
+
272
+ def source
273
+ @source ||= SourceFile.new(
274
+ data.fetch("location", {}).fetch("path", ""),
275
+ filesystem,
276
+ )
277
+ end
278
+
279
+ def location
280
+ @location ||=
281
+ Location.new(
282
+ source.buffer,
283
+ data["location"],
284
+ )
285
+ end
286
+
287
+ def other_locations
288
+ @other_locations ||=
289
+ begin
290
+ data.fetch("other_locations", []).map do |loc|
291
+ [SourceFile.new(loc["path"], filesystem), loc]
292
+ end.to_h
293
+ end
294
+ end
295
+
296
+ def categories
297
+ data.fetch("categories", [])
298
+ end
299
+
300
+ def engine_name
301
+ data["engine_name"]
302
+ end
303
+
304
+ private
305
+
306
+ attr_reader :data, :filesystem
307
+
308
+ def markdown(text)
309
+ html = Redcarpet::Render::HTML.new(
310
+ escape_html: false,
311
+ link_attributes: { target: "_blank" },
312
+ )
313
+ redcarpet = Redcarpet::Markdown.new(html, MARKDOWN_CONFIG)
314
+ redcarpet.render(text)
315
+ end
316
+ end
317
+
318
+ class IssueCollection
319
+ def initialize(filesystem)
320
+ @collection = []
321
+ @filesystem = filesystem
322
+ end
323
+
324
+ def each(&block)
325
+ collection.each(&block)
326
+ end
327
+
328
+ def <<(issue)
329
+ if issue.is_a? Hash
330
+ issue = Issue.new(issue, filesystem)
331
+ end
332
+ collection.push(issue)
333
+ end
334
+
335
+ def any?
336
+ collection.any?
337
+ end
338
+
339
+ def syntaxes
340
+ collection.flat_map do |issue|
341
+ issue.source.syntaxes
342
+ end.uniq.sort
343
+ end
344
+
345
+ def categories
346
+ collection.flat_map(&:categories).uniq.sort
347
+ end
348
+
349
+ def engines
350
+ collection.map(&:engine_name).uniq.compact.sort
351
+ end
352
+
353
+ private
354
+
355
+ attr_reader :collection, :filesystem
356
+ end
357
+
358
+ class ReportTemplate
359
+ include ERB::Util
360
+ attr_reader :issues
361
+
362
+ TEMPLATE_PATH = File.expand_path(File.join(File.dirname(__FILE__), "templates/html.erb"))
363
+
364
+ def initialize(issues, filesystem)
365
+ @issues = issues
366
+ @filesystem = filesystem
367
+ end
368
+
369
+ def render
370
+ template = File.read(TEMPLATE_PATH)
371
+ ERB.new(template, nil, "-").result(binding)
372
+ end
373
+
374
+ def project_name
375
+ File.basename(filesystem.root)
376
+ end
377
+
378
+ def param(str)
379
+ str.downcase.gsub(/\s+/, "-")
380
+ end
381
+
382
+ def params(values)
383
+ values.map { |c| param c }.join(" ")
384
+ end
385
+
386
+ private
387
+
388
+ attr_reader :filesystem
389
+ end
390
+
391
+ def finished
392
+ puts ReportTemplate.new(issues, @filesystem).render
393
+ end
394
+
395
+ def failed(_)
396
+ exit 1
397
+ end
398
+
399
+ private
400
+
401
+ def issues
402
+ @issues ||= IssueCollection.new(@filesystem)
403
+ end
404
+
405
+ def warnings
406
+ @warnings ||= []
407
+ end
408
+
409
+ def measurements
410
+ @measurements ||= []
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end