v2 / cmd / tools / vdoc / document / doc.v
594 lines · 576 sloc · 15.86 KB · 57238a1c6da6e94fbe5373add35e273d4430d34c
Raw
1module document
2
3import os
4import time
5import v.ast
6import v.checker
7import v.fmt
8import v.parser
9import v.pref
10import v.scanner
11import v.token
12
13// SymbolKind categorizes the symbols it documents.
14// The names are intentionally not in order as a guide when sorting the nodes.
15pub enum SymbolKind {
16 none
17 const_group
18 constant
19 variable
20 function
21 method
22 interface
23 typedef
24 enum
25 enum_field
26 struct
27 struct_field
28}
29
30pub enum Platform {
31 auto
32 ios
33 macos
34 linux
35 windows
36 freebsd
37 openbsd
38 netbsd
39 dragonfly
40 js // for interoperability in prefs.OS
41 android
42 termux // like android, but note that termux is running on devices natively, not cross compiling from other platforms
43 solaris
44 serenity
45 plan9
46 vinix
47 haiku
48 raw
49 cross // TODO: add functionality for v doc -cross whenever possible
50}
51
52// copy of pref.os_from_string
53pub fn platform_from_string(platform_str string) !Platform {
54 match platform_str {
55 'all', 'cross' { return .cross }
56 'linux' { return .linux }
57 'windows' { return .windows }
58 'ios' { return .ios }
59 'macos' { return .macos }
60 'freebsd' { return .freebsd }
61 'openbsd' { return .openbsd }
62 'netbsd' { return .netbsd }
63 'dragonfly' { return .dragonfly }
64 'js' { return .js }
65 'solaris' { return .solaris }
66 'serenity' { return .serenity }
67 'plan9' { return .plan9 }
68 'vinix' { return .vinix }
69 'android' { return .android }
70 'termux' { return .termux }
71 'haiku' { return .haiku }
72 'nix' { return .linux }
73 '' { return .auto }
74 else { return error('vdoc: invalid platform `${platform_str}`') }
75 }
76}
77
78pub fn platform_from_filename(filename string) Platform {
79 suffix := filename.all_after_last('_').all_before('.c.v')
80 mut platform := platform_from_string(suffix) or { Platform.cross }
81 if platform == .auto {
82 platform = .cross
83 }
84 return platform
85}
86
87pub fn (sk SymbolKind) str() string {
88 return match sk {
89 .const_group { 'Constants' }
90 .function, .method { 'fn' }
91 .interface { 'interface' }
92 .typedef { 'type' }
93 .enum { 'enum' }
94 .struct { 'struct' }
95 else { '' }
96 }
97}
98
99@[minify]
100pub struct Doc {
101pub mut:
102 prefs &pref.Preferences = new_vdoc_preferences()
103 base_path string
104 table &ast.Table = ast.new_table()
105 checker checker.Checker = checker.Checker{
106 table: unsafe { nil }
107 pref: unsafe { nil }
108 }
109 fmt fmt.Fmt
110 filename string
111 pos int
112 pub_only bool = true
113 with_comments bool = true
114 with_pos bool
115 with_head bool = true
116 is_vlib bool
117 time_generated time.Time
118 head DocNode
119 contents map[string]DocNode
120 scoped_contents map[string]DocNode
121 parent_mod_name string
122 orig_mod_name string
123 extract_vars bool
124 filter_symbol_names []string
125 common_symbols []string
126 platform Platform
127}
128
129@[minify]
130pub struct DocNode {
131pub mut:
132 name string
133 content string
134 comments []DocComment
135 pos token.Pos
136 file_path string
137 kind SymbolKind
138 tags []string
139 parent_name string
140 return_type string
141 children []DocNode
142 attrs map[string]string @[json: attributes]
143 from_scope bool
144 is_pub bool @[json: public]
145 platform Platform
146 is_readme bool
147 frontmatter map[string]string
148}
149
150// new_vdoc_preferences creates a new instance of pref.Preferences tailored for v.doc.
151pub fn new_vdoc_preferences() &pref.Preferences {
152 // vdoc should be able to parse as much user code as possible
153 // so its preferences should be permissive:
154 mut pref_ := &pref.Preferences{
155 enable_globals: true
156 is_fmt: true
157 is_vdoc: true
158 }
159 pref_.fill_with_defaults()
160 return pref_
161}
162
163// new creates a new instance of a `Doc` struct.
164pub fn new(input_path string) Doc {
165 mut d := Doc{
166 base_path: os.real_path(input_path)
167 table: ast.new_table()
168 head: DocNode{}
169 contents: map[string]DocNode{}
170 time_generated: time.now()
171 }
172 d.fmt = fmt.Fmt{
173 pref: d.prefs
174 indent: 0
175 is_debug: false
176 table: d.table
177 }
178 d.checker = checker.new_checker(d.table, d.prefs)
179 return d
180}
181
182// stmt reads the data of an `ast.Stmt` node and returns a `DocNode`.
183// An option error is thrown if the symbol is not exposed to the public
184// (when `pub_only` is enabled) or the content's of the AST node is empty.
185pub fn (mut d Doc) stmt(mut stmt ast.Stmt, filename string) !DocNode {
186 mut name := d.stmt_name(stmt)
187 if name in d.common_symbols {
188 return error('already documented')
189 }
190 if name.starts_with(d.orig_mod_name + '.') {
191 name = name.all_after(d.orig_mod_name + '.')
192 }
193 mut node := DocNode{
194 name: name
195 content: d.stmt_signature(stmt)
196 pos: stmt.pos
197 file_path: os.join_path(d.base_path, filename)
198 is_pub: d.stmt_pub(stmt)
199 platform: platform_from_filename(filename)
200 }
201 if (!node.is_pub && d.pub_only) || stmt is ast.GlobalDecl {
202 return error('symbol ${node.name} not public')
203 }
204 if node.name.starts_with(d.orig_mod_name + '.') {
205 node.name = node.name.all_after(d.orig_mod_name + '.')
206 }
207 if node.name == '' && node.comments.len == 0 && node.content.len == 0 {
208 return error('empty stmt')
209 }
210 match mut stmt {
211 ast.ConstDecl {
212 node.kind = .const_group
213 node.parent_name = 'Constants'
214 if d.extract_vars {
215 for mut field in stmt.fields {
216 ret_type := if field.typ == 0 {
217 d.expr_typ_to_string(mut field.expr)
218 } else {
219 d.type_to_str(field.typ)
220 }
221 node.children << DocNode{
222 name: field.name.all_after(d.orig_mod_name + '.')
223 kind: .constant
224 pos: field.pos
225 return_type: ret_type
226 }
227 }
228 }
229 }
230 ast.EnumDecl {
231 node.kind = .enum
232 if d.extract_vars {
233 for mut field in stmt.fields {
234 ret_type := if field.has_expr {
235 d.expr_typ_to_string(mut field.expr)
236 } else {
237 'int'
238 }
239 node.children << DocNode{
240 name: field.name
241 kind: .enum_field
242 parent_name: node.name
243 pos: field.pos
244 return_type: ret_type
245 }
246 }
247 }
248 for sa in stmt.attrs {
249 node.attrs[sa.name] = if sa.has_at { '@[${sa.str()}]' } else { '[${sa.str()}]' }
250 node.tags << node.attrs[sa.name]
251 }
252 }
253 ast.InterfaceDecl {
254 node.kind = .interface
255 }
256 ast.StructDecl {
257 node.kind = .struct
258 if d.extract_vars {
259 for mut field in stmt.fields {
260 ret_type := if field.typ == 0 && field.has_default_expr {
261 d.expr_typ_to_string(mut field.default_expr)
262 } else {
263 d.type_to_str(field.typ)
264 }
265 node.children << DocNode{
266 name: field.name
267 kind: .struct_field
268 parent_name: node.name
269 pos: field.pos
270 return_type: ret_type
271 }
272 }
273 }
274 for sa in stmt.attrs {
275 node.attrs[sa.name] = if sa.has_at { '@[${sa.str()}]' } else { '[${sa.str()}]' }
276 node.tags << node.attrs[sa.name]
277 }
278 }
279 ast.TypeDecl {
280 node.kind = .typedef
281 }
282 ast.FnDecl {
283 if stmt.is_deprecated {
284 for sa in stmt.attrs {
285 if sa.name.starts_with('deprecated') {
286 node.tags << sa.str()
287 }
288 }
289 }
290 if stmt.is_unsafe {
291 node.tags << 'unsafe'
292 }
293 node.kind = .function
294 node.return_type = d.type_to_str(stmt.return_type)
295 if stmt.receiver.typ !in [0, 1] {
296 method_parent := d.type_to_str(stmt.receiver.typ)
297 node.kind = .method
298 if !stmt.is_static_type_method {
299 node.parent_name = method_parent
300 } else {
301 node.parent_name = ''
302 }
303 }
304 if d.extract_vars {
305 for param in stmt.params {
306 node.children << DocNode{
307 name: param.name
308 kind: .variable
309 parent_name: node.name
310 pos: param.pos
311 attrs: {
312 'mut': param.is_mut.str()
313 }
314 return_type: d.type_to_str(param.typ)
315 }
316 }
317 }
318 }
319 else {
320 return error('invalid stmt type to document')
321 }
322 }
323
324 included := node.name in d.filter_symbol_names || node.parent_name in d.filter_symbol_names
325 if d.filter_symbol_names.len != 0 && !included {
326 return error('not included in the list of symbol names')
327 }
328 if d.prefs.os == .all {
329 d.common_symbols << node.name
330 }
331 return node
332}
333
334// file_ast reads the contents of `ast.File` and returns a map of `DocNode`s.
335pub fn (mut d Doc) file_ast(mut file_ast ast.File) map[string]DocNode {
336 mut contents := map[string]DocNode{}
337 d.fmt.file = file_ast
338 d.fmt.set_current_module_name(d.orig_mod_name)
339 d.fmt.process_file_imports(file_ast)
340 mut last_import_stmt_idx := 0
341 for sidx, stmt in file_ast.stmts {
342 if stmt is ast.Import {
343 last_import_stmt_idx = sidx
344 }
345 }
346 mut preceding_comments := []DocComment{}
347 mut collect_post_module_comments := false
348 mut post_module_comments := []DocComment{}
349 // mut imports_section := true
350 for sidx, mut stmt in file_ast.stmts {
351 if mut stmt is ast.ExprStmt {
352 // Collect comments
353 if mut stmt.expr is ast.Comment {
354 comment := ast_comment_to_doc_comment(stmt.expr)
355 if collect_post_module_comments {
356 post_module_comments << comment
357 } else {
358 preceding_comments << comment
359 }
360 continue
361 } else if stmt.expr is ast.IfExpr && stmt.expr.is_comptime {
362 comments := ast_comments_to_doc_comments(stmt.expr.post_comments)
363 if collect_post_module_comments {
364 post_module_comments << comments
365 } else {
366 preceding_comments << comments
367 }
368 continue
369 }
370 }
371 // TODO: Fetch head comment once
372 if mut stmt is ast.Module {
373 if !d.with_head {
374 continue
375 }
376 // the previous comments were probably a copyright/license one
377 module_comment := merge_doc_comments(preceding_comments)
378 if !d.is_vlib && !module_comment.starts_with('Copyright (c)') && module_comment != '' {
379 d.head.comments << preceding_comments
380 }
381 preceding_comments = []
382 collect_post_module_comments = true
383 continue
384 }
385 if collect_post_module_comments {
386 if post_module_comments.len > 0 {
387 last_post_module_comment := post_module_comments[post_module_comments.len - 1]
388 if stmt is ast.Import || last_post_module_comment.pos.line_nr + 1 < stmt.pos.line_nr {
389 d.head.comments << post_module_comments
390 } else {
391 preceding_comments << post_module_comments
392 }
393 post_module_comments = []
394 }
395 collect_post_module_comments = false
396 }
397 if last_import_stmt_idx > 0 && sidx == last_import_stmt_idx {
398 // the accumulated comments were interspersed before/between the imports;
399 // just add them all to the module comments:
400 if d.with_head {
401 d.head.comments << preceding_comments
402 }
403 preceding_comments = []
404 // imports_section = false
405 }
406 if stmt is ast.Import {
407 continue
408 }
409 mut node := d.stmt(mut stmt, os.base(file_ast.path)) or {
410 preceding_comments = []
411 continue
412 }
413 if node.parent_name !in contents {
414 parent_node_kind := if node.parent_name == 'Constants' {
415 SymbolKind.const_group
416 } else {
417 SymbolKind.typedef
418 }
419 contents[node.parent_name] = DocNode{
420 name: node.parent_name
421 kind: parent_node_kind
422 }
423 }
424 if d.with_comments && preceding_comments.len > 0 {
425 node.comments << preceding_comments
426 }
427 preceding_comments = []
428 if node.parent_name.len > 0 {
429 parent_name := node.parent_name
430 if node.parent_name == 'Constants' {
431 node.parent_name = ''
432 }
433 contents[parent_name].children << node
434 } else {
435 contents[node.name] = node
436 }
437 }
438 if collect_post_module_comments && post_module_comments.len > 0 {
439 d.head.comments << post_module_comments
440 }
441 d.fmt.mod2alias = map[string]string{}
442 if contents[''].kind != .const_group {
443 contents.delete('')
444 }
445 return contents
446}
447
448// file_ast_with_pos has the same function as the `file_ast` but
449// instead returns a list of variables in a given offset-based position.
450pub fn (mut d Doc) file_ast_with_pos(mut file_ast ast.File, pos int) map[string]DocNode {
451 lscope := file_ast.scope.innermost(pos)
452 mut contents := map[string]DocNode{}
453 for name, val in lscope.objects {
454 if val !is ast.Var {
455 continue
456 }
457 mut vr_data := val as ast.Var
458 l_node := DocNode{
459 name: name
460 pos: vr_data.pos
461 file_path: file_ast.path
462 from_scope: true
463 kind: .variable
464 return_type: d.expr_typ_to_string(mut vr_data.expr)
465 }
466 contents[l_node.name] = l_node
467 }
468 return contents
469}
470
471// generate is a `Doc` method that will start documentation
472// process based on a file path provided.
473pub fn (mut d Doc) generate() ! {
474 // get all files
475 d.base_path = if os.is_dir(d.base_path) {
476 d.base_path
477 } else {
478 os.real_path(os.dir(d.base_path))
479 }
480 d.is_vlib = d.base_path.contains('vlib')
481 project_files := os.ls(d.base_path) or { return err }
482 v_files := d.prefs.should_compile_filtered_files(d.base_path, project_files)
483 if v_files.len == 0 {
484 eprintln('vdoc: No valid V files were found. Skipping folder: ${d.base_path}.')
485 return
486 }
487 // parse files
488 mut comments_mode := scanner.CommentsMode.skip_comments
489 if d.with_comments {
490 comments_mode = .parse_comments
491 }
492 mut file_asts := []ast.File{}
493 for i, file_path in v_files {
494 if i == 0 {
495 d.parent_mod_name = get_parent_mod(d.base_path) or { '' }
496 }
497 file_asts << parser.parse_file(file_path, mut d.table, comments_mode, d.prefs)
498 }
499 mut generated_file_asts := []&ast.File{}
500 parser.append_codegen_files(mut generated_file_asts)
501 for generated_file_ast in generated_file_asts {
502 file_asts << *generated_file_ast
503 }
504 return d.file_asts(mut file_asts)
505}
506
507// file_asts has the same function as the `file_ast` function but
508// accepts an array of `ast.File` and throws an error if necessary.
509pub fn (mut d Doc) file_asts(mut file_asts []ast.File) ! {
510 mut fname_has_set := false
511 d.orig_mod_name = file_asts[0].mod.name
512 for i, mut file_ast in file_asts {
513 if d.filename.len > 0 && file_ast.path.contains(d.filename) && !fname_has_set {
514 d.filename = file_ast.path
515 fname_has_set = true
516 }
517 if d.with_head && i == 0 {
518 mut module_name := file_ast.mod.name
519 if module_name != file_ast.mod.short_name
520 && !module_name.ends_with('.${file_ast.mod.short_name}') {
521 // qualify_module resolved by path instead of module name,
522 // use the short name from the source
523 module_name = file_ast.mod.short_name
524 }
525 d.head = DocNode{
526 name: module_name
527 content: 'module ${module_name}'
528 kind: .none
529 }
530 } else if file_ast.mod.name != d.orig_mod_name {
531 continue
532 }
533 if file_ast.path == d.filename {
534 d.checker.check(mut file_ast)
535 d.scoped_contents = d.file_ast_with_pos(mut file_ast, d.pos)
536 }
537 contents := d.file_ast(mut file_ast)
538 for name, node in contents {
539 if name !in d.contents {
540 d.contents[name] = node
541 continue
542 }
543 if d.contents[name].kind == .typedef && node.kind !in [.typedef, .none] {
544 old_children := d.contents[name].children.clone()
545 d.contents[name] = node
546 d.contents[name].children = old_children
547 }
548 if d.contents[name].kind != .none || node.kind == .none {
549 d.contents[name].children << node.children
550 d.contents[name].children.arrange()
551 }
552 }
553 }
554 if d.filter_symbol_names.len != 0 && d.contents.len != 0 {
555 for filter_name in d.filter_symbol_names {
556 if filter_name !in d.contents {
557 return error('vdoc: `${filter_name}` symbol in module `${d.orig_mod_name}` not found')
558 }
559 }
560 }
561 d.time_generated = time.now()
562}
563
564// generate documents a certain file directory and returns an
565// instance of `Doc` if it is successful. Otherwise, it will throw an error.
566pub fn generate(input_path string, pub_only bool, with_comments bool, platform Platform, filter_symbol_names ...string) !Doc {
567 if platform == .js {
568 return error('vdoc: Platform `${platform}` is not supported.')
569 }
570 mut d := new(input_path)
571 d.pub_only = pub_only
572 d.with_comments = with_comments
573 d.filter_symbol_names = filter_symbol_names.filter(it.len != 0)
574 d.prefs.os = if platform == .auto {
575 pref.get_host_os()
576 } else {
577 unsafe { pref.OS(int(platform)) }
578 }
579 d.generate()!
580 return d
581}
582
583// generate_with_pos has the same function as the `generate` function but
584// accepts an offset-based position and enables the comments by default.
585pub fn generate_with_pos(input_path string, filename string, pos int) !Doc {
586 mut d := new(input_path)
587 d.pub_only = false
588 d.with_comments = true
589 d.with_pos = true
590 d.filename = filename
591 d.pos = pos
592 d.generate()!
593 return d
594}
595