ci: do not error when nothing with nitunit_some
[nit.git] / src / metrics / readme_metrics.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Collect common metrics about README files
16 #
17 # Also works with generic Markdown files.
18 module readme_metrics
19
20 import metrics_base
21 import model::model_collect
22 import markdown
23
24 redef class ToolContext
25
26 # README related metrics phase
27 var readme_metrics_phase: Phase = new ReadmeMetricsPhase(self, null)
28 end
29
30 # Extract metrics about README files
31 private class ReadmeMetricsPhase
32 super Phase
33
34 redef fun process_mainmodule(mainmodule, given_mmodules) do
35 if not toolcontext.opt_readme.value and not toolcontext.opt_all.value then return
36
37 print toolcontext.format_h1("\n# ReadMe metrics")
38 var model = toolcontext.modelbuilder.model
39
40 var metrics = new ReadmeMetrics
41 metrics.collect_metrics(model.mpackages)
42 metrics.to_console(toolcontext)
43
44 var csv = toolcontext.opt_csv.value
45 if csv then metrics.to_csv.write_to_file("{toolcontext.opt_dir.value or else "metrics"}/readme.csv")
46 end
47 end
48
49 # A Markdown decorator that collects metrics about a Readme content
50 class MetricsDecorator
51 super HTMLDecorator
52
53 # Count blocks
54 var block_counter = new Counter[String]
55
56 # Count sections
57 var headline_counter = new Counter[Int]
58
59 redef fun add_ruler(v, block) do
60 block_counter.inc block.class_name
61 super
62 end
63
64 redef fun add_headline(v, block) do
65 block_counter.inc block.class_name
66 headline_counter.inc block.depth
67 super
68 end
69
70 redef fun add_paragraph(v, block) do
71 block_counter.inc block.class_name
72 super
73 end
74
75 redef fun add_code(v, block) do
76 block_counter.inc block.class_name
77 super
78 end
79
80 redef fun add_blockquote(v, block) do
81 block_counter.inc block.class_name
82 super
83 end
84
85 redef fun add_unorderedlist(v, block) do
86 block_counter.inc block.class_name
87 super
88 end
89
90 redef fun add_orderedlist(v, block) do
91 block_counter.inc block.class_name
92 super
93 end
94
95 redef fun add_listitem(v, block) do
96 block_counter.inc block.class_name
97 super
98 end
99
100 redef fun add_image(v, link, name, comment) do
101 block_counter.inc "Image"
102 super
103 end
104
105 redef fun add_link(v, link, name, comment) do
106 block_counter.inc "Link"
107 super
108 end
109
110 redef fun add_span_code(v, text, from, to) do
111 block_counter.inc "SpanCode"
112 super
113 end
114 end
115
116 # All metrics about the readmes
117 class ReadmeMetrics
118 super HashMap[MPackage, ReadmeMetric]
119
120 # Collect all metric names from submetrics
121 fun metrics_names: ArraySet[String] do
122 var keys = new ArraySet[String]
123 keys.add "MPackage"
124 for mpackage, values in self do
125 keys.add_all values.keys
126 end
127 return keys
128 end
129
130 # Render `self` as a CsvDocument
131 fun to_csv: CsvDocument do
132 var doc = new CsvDocument
133 doc.header = metrics_names.to_a
134
135 var metrics = metrics_names
136 for mpackage in self.keys do
137 doc.records.add self[mpackage].to_csv_record(metrics)
138 end
139 return doc
140 end
141
142 # Print `self` into stdout
143 fun to_console(toolcontext: ToolContext) do
144 for mpackage, values in self do
145 if not values.has_readme then continue
146 values.to_console(toolcontext)
147 end
148 end
149
150 # Collect metrics for all `mpackages`
151 fun collect_metrics(mpackages: Collection[MPackage]) do
152 for mpackage in mpackages do
153 var metric = new ReadmeMetric(mpackage)
154 metric.collect_metrics
155 self[mpackage] = metric
156 end
157 end
158 end
159
160 # Readme metrics associated to a Package
161 class ReadmeMetric
162 super HashMap[String, Int]
163
164 # Package this Readme is about
165 var mpackage: MPackage
166
167 # Render `self` as a CsvDocument record
168 fun to_csv_record(keys: ArraySet[String]): Array[String] do
169 var record = new Array[String]
170 record.add mpackage.full_name
171 for key in keys do
172 if key == keys.first then continue
173 var value = if self.has_key(key) then self[key] else 0
174 record.add value.to_s
175 end
176 return record
177 end
178
179 # Return the value associated with `key` or `0`.
180 fun value_or_zero(key: String): Int do
181 return if self.has_key(key) then self[key] else 0
182 end
183
184 # Print `self` on stdout
185 fun to_console(toolcontext: ToolContext) do
186 print toolcontext.format_h2("\n ## package {mpackage} ({readme_path or else "no readme"})")
187 for key, value in self do
188 print " * {key} {value}"
189 end
190 end
191
192 # Collect metrics about `mpackage`
193 fun collect_metrics do
194 if not has_package_dir then
195 print "Warning: no source file for `{mpackage}`"
196 self["has_package"] = 0
197 return
198 end
199 self["has_package"] = 1
200
201 if not has_readme then
202 print "Warning: no readme file for `{mpackage}`"
203 self["has_readme"] = 0
204 return
205 end
206 self["has_readme"] = 1
207 self["md_lines"] = md_lines.length
208
209 collect_sections_metrics
210 collect_blocs_metrics
211 end
212
213 # Path to the package
214 var package_path: nullable SourceFile is lazy do return mpackage.location.file
215
216 # Is `mpackage` in its own directory?
217 var has_package_dir: Bool is lazy do
218 var path = package_path
219 if path == null then return false
220 return not path.filename.has_suffix(".nit")
221 end
222
223 # Return the path to the `mpackage` Readme file
224 var readme_path: nullable String is lazy do
225 var package_path = self.package_path
226 if package_path == null then return null
227 return package_path.filename / "README.md"
228 end
229
230 # Does `mpackage` has a Readme file?
231 var has_readme: Bool is lazy do
232 var readme_path = self.readme_path
233 if readme_path == null then return false
234 return readme_path.to_s.file_exists
235 end
236
237 # Read markdown lines
238 #
239 # Returns an empty array if the Readme does not exist.
240 var md_lines: Array[String] is lazy do
241 var path = readme_path
242 if path == null then return new Array[String]
243 return path.to_path.read_lines
244 end
245
246 # Markdown decorator used to visit Markdown content
247 var md_decorator: MetricsDecorator is lazy do
248 var md_decorator = new MetricsDecorator
249 var md_proc = new MarkdownProcessor
250 md_proc.decorator = md_decorator
251 md_proc.process(md_lines.join("\n"))
252 return md_decorator
253 end
254
255 # Collect metrics related to section headings
256 fun collect_sections_metrics do
257 self["nb_section"] = md_decorator.headline_counter.sum
258 for lvl, count in md_decorator.headline_counter do
259 self["HL {lvl}"] = count
260 end
261 end
262
263 # Collect metrics related to Markdown blocks
264 fun collect_blocs_metrics do
265 self["md_blocks"] = md_decorator.block_counter.sum
266 for block, count in md_decorator.block_counter do
267 self[block] = count
268 end
269 end
270 end