Merge: fix ci 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 markdown2
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 MarkdownMetrics
51 super MdVisitor
52
53 # Count nodes
54 var nodes_counter = new Counter[String]
55
56 # Count heading levels
57 var headings_counter = new Counter[Int]
58
59 redef fun visit(node) do
60 nodes_counter.inc node.class_name
61 if node isa MdHeading then
62 headings_counter.inc node.level
63 end
64 node.visit_all self
65 end
66 end
67
68 # All metrics about the readmes
69 class ReadmeMetrics
70 super HashMap[MPackage, ReadmeMetric]
71
72 # Collect all metric names from submetrics
73 fun metrics_names: ArraySet[String] do
74 var keys = new ArraySet[String]
75 keys.add "MPackage"
76 for mpackage, values in self do
77 keys.add_all values.keys
78 end
79 return keys
80 end
81
82 # Render `self` as a CsvDocument
83 fun to_csv: CsvDocument do
84 var doc = new CsvDocument
85 doc.header = metrics_names.to_a
86
87 var metrics = metrics_names
88 for mpackage in self.keys do
89 doc.records.add self[mpackage].to_csv_record(metrics)
90 end
91 return doc
92 end
93
94 # Print `self` into stdout
95 fun to_console(toolcontext: ToolContext) do
96 for mpackage, values in self do
97 if not values.has_readme then continue
98 values.to_console(toolcontext)
99 end
100 end
101
102 # Collect metrics for all `mpackages`
103 fun collect_metrics(mpackages: Collection[MPackage]) do
104 for mpackage in mpackages do
105 var metric = new ReadmeMetric(mpackage)
106 metric.collect_metrics
107 self[mpackage] = metric
108 end
109 end
110 end
111
112 # Readme metrics associated to a Package
113 class ReadmeMetric
114 super HashMap[String, Int]
115
116 # Package this Readme is about
117 var mpackage: MPackage
118
119 # Render `self` as a CsvDocument record
120 fun to_csv_record(keys: ArraySet[String]): Array[String] do
121 var record = new Array[String]
122 record.add mpackage.full_name
123 for key in keys do
124 if key == keys.first then continue
125 var value = if self.has_key(key) then self[key] else 0
126 record.add value.to_s
127 end
128 return record
129 end
130
131 # Return the value associated with `key` or `0`.
132 fun value_or_zero(key: String): Int do
133 return if self.has_key(key) then self[key] else 0
134 end
135
136 # Print `self` on stdout
137 fun to_console(toolcontext: ToolContext) do
138 print toolcontext.format_h2("\n ## package {mpackage} ({readme_path or else "no readme"})")
139 for key, value in self do
140 print " * {key} {value}"
141 end
142 end
143
144 # Collect metrics about `mpackage`
145 fun collect_metrics do
146 if not has_package_dir then
147 print "Warning: no source file for `{mpackage}`"
148 self["has_package"] = 0
149 return
150 end
151 self["has_package"] = 1
152
153 if not has_readme then
154 print "Warning: no readme file for `{mpackage}`"
155 self["has_readme"] = 0
156 return
157 end
158 self["has_readme"] = 1
159 self["md_lines"] = md_lines.length
160
161 var parser = new MdParser
162 var node = parser.parse(md_lines.join("\n"))
163 var v = new MarkdownMetrics
164 v.enter_visit(node)
165 for md_node, value in v.nodes_counter do
166 self[md_node] = value
167 end
168 for level, value in v.headings_counter do
169 self["HL {level}"] = value
170 end
171 end
172
173 # Path to the package
174 var package_path: nullable SourceFile is lazy do return mpackage.location.file
175
176 # Is `mpackage` in its own directory?
177 var has_package_dir: Bool is lazy do
178 var path = package_path
179 if path == null then return false
180 return not path.filename.has_suffix(".nit")
181 end
182
183 # Return the path to the `mpackage` Readme file
184 var readme_path: nullable String is lazy do
185 var package_path = self.package_path
186 if package_path == null then return null
187 return package_path.filename / "README.md"
188 end
189
190 # Does `mpackage` has a Readme file?
191 var has_readme: Bool is lazy do
192 var readme_path = self.readme_path
193 if readme_path == null then return false
194 return readme_path.to_s.file_exists
195 end
196
197 # Read markdown lines
198 #
199 # Returns an empty array if the Readme does not exist.
200 var md_lines: Array[String] is lazy do
201 var path = readme_path
202 if path == null then return new Array[String]
203 return path.to_path.read_lines
204 end
205 end