Skip to content

Commit cb8cb27

Browse files
committed
Merge remote-tracking branch 'origin/master' into feature/parallelism
2 parents 3856485 + 56e26de commit cb8cb27

File tree

7 files changed

+296
-55
lines changed

7 files changed

+296
-55
lines changed

bin/cm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,26 @@ desc 'Generate HTML documentation'
1414
long_desc 'Generate HTML docs from a Docurium config file'
1515
command :doc do |c|
1616
c.flag :for, :desc => "The version to generate", :multiple => true
17+
c.switch [:n, "dry-run"], :desc => "Dry-run"
18+
c.switch [:d, "debug"], :desc => "Enable debug log"
19+
c.flag "debug-file", :desc => "Enable debug output for header", :multiple => true
20+
c.flag "debug-function", :desc => "Show debug output when processing function", :multiple => true
21+
c.flag "debug-type", :desc => "Show debug output when processing type", :multiple => true
1722
c.action do |global_options,options,args|
1823
file = args.first
1924
Docurium::CLI.doc(file, options)
2025
end
2126
end
2227

28+
desc 'Check documentation for warnings'
29+
long_desc 'Check a project\'s documentation for issues'
30+
command :check do |c|
31+
c.action do |global_options,options,args|
32+
file = args.first
33+
Docurium::CLI.check(file, options)
34+
end
35+
end
36+
2337
desc 'Generate Docurium config file template'
2438
long_desc 'Generate Docurium config file template'
2539
command :gen do |c|

lib/docurium.rb

Lines changed: 162 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'rocco'
55
require 'docurium/version'
66
require 'docurium/layout'
7+
require 'docurium/debug'
78
require 'libdetect'
89
require 'docurium/docparser'
910
require 'pp'
@@ -20,12 +21,13 @@
2021
class Docurium
2122
attr_accessor :branch, :output_dir, :data, :head_data
2223

23-
def initialize(config_file, repo = nil)
24+
def initialize(config_file, cli_options = {}, repo = nil)
2425
raise "You need to specify a config file" if !config_file
2526
raise "You need to specify a valid config file" if !valid_config(config_file)
2627
@sigs = {}
2728
@head_data = nil
2829
@repo = repo || Rugged::Repository.discover(config_file)
30+
@cli_options = cli_options
2931
end
3032

3133
def init_data(version = 'HEAD')
@@ -118,7 +120,7 @@ def generate_doc_for(version)
118120
index = Rugged::Index.new
119121
read_subtree(index, version, option_version(version, 'input', ''))
120122

121-
data = parse_headers(index, version, reference)
123+
data = parse_headers(index, version)
122124
examples = format_examples!(data, version)
123125
[data, examples]
124126
end
@@ -144,14 +146,14 @@ def process_project(versions)
144146
end
145147
end
146148

147-
def generate_docs(options)
149+
def generate_docs
148150
output_index = Rugged::Index.new
149151
write_site(output_index)
150152
@tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache'))
151153
versions = get_versions
152154
versions << 'HEAD'
153155
# If the user specified versions, validate them and overwrite
154-
if !(vers = options[:for]).empty?
156+
if !(vers = (@cli_options[:for] || [])).empty?
155157
vers.each do |v|
156158
next if versions.include?(v)
157159
puts "Unknown version #{v}"
@@ -161,13 +163,16 @@ def generate_docs(options)
161163
end
162164

163165
process_project(versions) do |i, version, result|
164-
print "Writing documentation [#{i}/#{versions.count}]\r"
165166
data, examples = result
166-
167167
sha = @repo.write(data.to_json, :blob)
168-
output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644)
169-
examples.each do |path, id|
170-
output_index.add(:path => path, :oid => id, :mode => 0100644)
168+
169+
print "Generating documentation [#{i}/#{versions.count}]\r"
170+
171+
unless dry_run?
172+
output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644)
173+
examples.each do |path, id|
174+
output_index.add(:path => path, :oid => id, :mode => 0100644)
175+
end
171176
end
172177
end
173178

@@ -176,6 +181,8 @@ def generate_docs(options)
176181
show_warnings(head_data)
177182
end
178183

184+
return if dry_run?
185+
179186
# We tally the signatures in the order they finished, which is
180187
# arbitrary due to the concurrency, so we need to sort them once
181188
# they've finished.
@@ -228,29 +235,122 @@ def force_utf8(data)
228235
end
229236
end
230237

231-
def show_warnings(data)
232-
out '* checking your api'
238+
class Warning
239+
class UnmatchedParameter < Warning
240+
def initialize(function, opts = {})
241+
super :unmatched_param, :function, function, opts
242+
end
243+
244+
def _message; "unmatched param"; end
245+
end
246+
247+
class SignatureChanged < Warning
248+
def initialize(function, opts = {})
249+
super :signature_changed, :function, function, opts
250+
end
251+
252+
def _message; "signature changed"; end
253+
end
254+
255+
class MissingDocumentation < Warning
256+
def initialize(type, identifier, opts = {})
257+
super :missing_documentation, type, identifier, opts
258+
end
259+
260+
def _message
261+
["%s %s is missing documentation", :type, :identifier]
262+
end
263+
end
264+
265+
WARNINGS = [
266+
:unmatched_param,
267+
:signature_changed,
268+
:missing_documentation,
269+
]
270+
271+
attr_reader :warning, :type, :identifier, :file, :line, :column
272+
273+
def initialize(warning, type, identifier, opts = {})
274+
raise ArgumentError.new("invalid warning class") unless WARNINGS.include?(warning)
275+
@warning = warning
276+
@type = type
277+
@identifier = identifier
278+
if type = opts.delete(:type)
279+
@file = type[:file]
280+
if input_dir = opts.delete(:input_dir)
281+
File.expand_path(File.join(input_dir, @file))
282+
end
283+
@file ||= "<missing>"
284+
@line = type[:line] || 1
285+
@column = type[:column] || 1
286+
end
287+
end
288+
289+
def message
290+
msg = self._message
291+
msg.shift % msg.map {|a| self.send(a).to_s } if msg.kind_of?(Array)
292+
end
293+
end
294+
295+
def collect_warnings(data)
296+
warnings = []
297+
input_dir = File.join(@project_dir, option_version("HEAD", 'input'))
233298

234299
# check for unmatched paramaters
235-
unmatched = []
236300
data[:functions].each do |f, fdata|
237-
unmatched << f if fdata[:comments] =~ /@param/
238-
end
239-
if unmatched.size > 0
240-
out ' - unmatched params in'
241-
unmatched.sort.each { |p| out ("\t" + p) }
301+
warnings << Warning::UnmatchedParameter.new(f, type: fdata, input_dir: input_dir) if fdata[:comments] =~ /@param/
242302
end
243303

244304
# check for changed signatures
245305
sigchanges = []
246306
@sigs.each do |fun, sig_data|
247-
if sig_data[:changes]['HEAD']
248-
sigchanges << fun
307+
warnings << Warning::SignatureChanged.new(fun) if sig_data[:changes]['HEAD']
308+
end
309+
310+
# check for undocumented things
311+
types = [:functions, :callbacks, :globals, :types]
312+
types.each do |type_id|
313+
under_type = type_id.tap {|t| break t.to_s[0..-2].to_sym }
314+
data[type_id].each do |ident, type|
315+
under_type = type[:type] if type_id == :types
316+
317+
warnings << Warning::MissingDocumentation.new(under_type, ident, type: type, input_dir: input_dir) if type[:description].empty?
318+
319+
case type[:type]
320+
when :struct
321+
if type[:fields]
322+
type[:fields].each do |field|
323+
warnings << Warning::MissingDocumentation.new(:field, "#{ident}.#{field[:name]}", type: type, input_dir: input_dir) if field[:comments].empty?
324+
end
325+
end
326+
end
249327
end
250328
end
251-
if sigchanges.size > 0
252-
out ' - signature changes in'
253-
sigchanges.sort.each { |p| out ("\t" + p) }
329+
warnings
330+
end
331+
332+
def check_warnings(options)
333+
versions = []
334+
versions << get_versions.pop
335+
versions << 'HEAD'
336+
337+
process_project(versions)
338+
339+
collect_warnings(head_data).each do |warning|
340+
puts "#{warning.file}:#{warning.line}:#{warning.column}: #{warning.message}"
341+
end
342+
end
343+
344+
def show_warnings(data)
345+
out '* checking your api'
346+
347+
collect_warnings(data).group_by {|w| w.warning }.each do |klass, klass_warnings|
348+
klass_warnings.group_by {|w| w.type }.each do |type, type_warnings|
349+
out " - " + type_warnings[0].message
350+
type_warnings.sort_by {|w| w.identifier }.each do |warning|
351+
out "\t" + warning.identifier
352+
end
353+
end
254354
end
255355
end
256356

@@ -269,10 +369,11 @@ def parse_headers(index, version)
269369
end
270370

271371
data = init_data(version)
272-
parser = DocParser.new
273-
headers.each do |header|
274-
records = parser.parse_file(header, files)
275-
update_globals!(data, records)
372+
DocParser.with_files(files, :prefix => version) do |parser|
373+
headers.each do |header|
374+
records = parser.parse_file(header, debug: interesting?(:file, header))
375+
update_globals!(data, records)
376+
end
276377
end
277378

278379
data[:groups] = group_functions!(data)
@@ -345,16 +446,20 @@ def valid_config(file)
345446
def group_functions!(data)
346447
func = {}
347448
data[:functions].each_pair do |key, value|
449+
debug_set interesting?(:function, key)
450+
debug "grouping #{key}: #{value}"
348451
if @options['prefix']
349452
k = key.gsub(@options['prefix'], '')
350453
else
351454
k = key
352455
end
353456
group, rest = k.split('_', 2)
457+
debug "grouped: k: #{k}, group: #{group}, rest: #{rest}"
354458
if group.empty?
355459
puts "empty group for function #{key}"
356460
next
357461
end
462+
debug "grouped: k: #{k}, group: #{group}, rest: #{rest}"
358463
data[:functions][key][:group] = group
359464
func[group] ||= []
360465
func[group] << key
@@ -400,6 +505,25 @@ def update_globals!(data, recs)
400505
md = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({}), :no_intra_emphasis => true)
401506
recs.each do |r|
402507

508+
types = %w(function file type).map(&:to_sym)
509+
dbg = false
510+
types.each do |t|
511+
dbg ||= if r[:type] == t and interesting?(t, r[:name])
512+
true
513+
elsif t == :file and interesting?(:file, r[:file])
514+
true
515+
elsif [:struct, :enum].include?(r[:type]) and interesting?(:type, r[:name])
516+
true
517+
else
518+
false
519+
end
520+
end
521+
522+
debug_set dbg
523+
524+
debug "processing record: #{r}"
525+
debug
526+
403527
# initialize filemap for this file
404528
file_map[r[:file]] ||= {
405529
:file => r[:file], :functions => [], :meta => {}, :lines => 0
@@ -507,6 +631,10 @@ def update_globals!(data, recs)
507631
# Anything else we want to record?
508632
end
509633

634+
debug "processed record: #{r}"
635+
debug
636+
637+
debug_restore
510638
end
511639

512640
data[:files] << file_map.values[0]
@@ -537,4 +665,12 @@ def write_site(index)
537665
def out(text)
538666
puts text
539667
end
668+
669+
def dry_run?
670+
@cli_options[:dry_run]
671+
end
672+
673+
def interesting?(type, what)
674+
@cli_options['debug'] || (@cli_options["debug-#{type}"] || []).include?(what)
675+
end
540676
end

lib/docurium/cli.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ class Docurium
22
class CLI
33

44
def self.doc(idir, options)
5+
doc = Docurium.new(idir, options)
6+
doc.generate_docs
7+
end
8+
9+
def self.check(idir, options)
510
doc = Docurium.new(idir)
6-
doc.generate_docs(options)
11+
doc.check_warnings(options)
712
end
813

914
def self.gen(file)

lib/docurium/debug.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
$debug_stack = [false]
2+
3+
def debug_enabled
4+
$debug_stack[-1]
5+
end
6+
7+
def debug(str = nil)
8+
puts str if debug_enabled
9+
end
10+
11+
def debug_enable
12+
$debug_stack.push true
13+
end
14+
15+
def debug_silence
16+
$debug_stack.push false
17+
end
18+
19+
def debug_set val
20+
$debug_stack.push val
21+
end
22+
23+
def debug_pass
24+
$debug_stack.push debug_enabled
25+
end
26+
27+
def debug_restore
28+
$debug_stack.pop
29+
end
30+
31+
def with_debug(&block)
32+
debug_enable
33+
block.call
34+
debug_restore
35+
end
36+
37+
def without_debug(&block)
38+
debug_silence
39+
block.call
40+
debug_restore
41+
end

0 commit comments

Comments
 (0)