module doc import os import time import v.ast import v.checker import v.fmt import v.parser import v.pref import v.scanner import v.token // SymbolKind categorizes the symbols it documents. // The names are intentionally not in order as a guide when sorting the nodes. pub enum SymbolKind { none_ const_group constant variable function method interface_ typedef enum_ enum_field struct_ struct_field } pub enum Platform { auto ios macos linux windows freebsd openbsd netbsd dragonfly js // for interoperability in prefs.OS android solaris haiku cross // TODO: add functionality for v doc -os cross whenever possible } // copy of pref.os_from_string pub fn platform_from_string(platform_str string) ?Platform { match platform_str { 'all', 'cross' { return .cross } 'linux' { return .linux } 'windows' { return .windows } 'ios' { return .ios } 'macos' { return .macos } 'freebsd' { return .freebsd } 'openbsd' { return .openbsd } 'netbsd' { return .netbsd } 'dragonfly' { return .dragonfly } 'js' { return .js } 'solaris' { return .solaris } 'android' { return .android } 'haiku' { return .haiku } 'linux_or_macos', 'nix' { return .linux } '' { return .auto } else { return error('vdoc: invalid platform `$platform_str`') } } } pub fn platform_from_filename(filename string) Platform { suffix := filename.all_after_last('_').all_before('.c.v') mut platform := platform_from_string(suffix) or { Platform.cross } if platform == .auto { platform = .cross } return platform } pub fn (sk SymbolKind) str() string { return match sk { .const_group { 'Constants' } .function, .method { 'fn' } .interface_ { 'interface' } .typedef { 'type' } .enum_ { 'enum' } .struct_ { 'struct' } else { '' } } } pub struct Doc { pub mut: prefs &pref.Preferences = new_vdoc_preferences() base_path string table &ast.Table = &ast.Table{} checker checker.Checker = checker.Checker{ table: 0 cur_fn: 0 pref: 0 } fmt fmt.Fmt filename string pos int pub_only bool = true with_comments bool = true with_pos bool with_head bool = true is_vlib bool time_generated time.Time head DocNode contents map[string]DocNode scoped_contents map[string]DocNode parent_mod_name string orig_mod_name string extract_vars bool filter_symbol_names []string common_symbols []string platform Platform } pub struct DocNode { pub mut: name string content string comments []DocComment pos token.Position file_path string kind SymbolKind deprecated bool parent_name string return_type string children []DocNode attrs map[string]string [json: attributes] from_scope bool is_pub bool [json: public] platform Platform } // new_vdoc_preferences creates a new instance of pref.Preferences tailored for v.doc. pub fn new_vdoc_preferences() &pref.Preferences { // vdoc should be able to parse as much user code as possible // so its preferences should be permissive: mut pref := &pref.Preferences{ enable_globals: true is_fmt: true } pref.fill_with_defaults() return pref } // new creates a new instance of a `Doc` struct. pub fn new(input_path string) Doc { mut d := Doc{ base_path: os.real_path(input_path) table: ast.new_table() head: DocNode{} contents: map[string]DocNode{} time_generated: time.now() } d.fmt = fmt.Fmt{ pref: d.prefs indent: 0 is_debug: false table: d.table } d.checker = checker.new_checker(d.table, d.prefs) return d } // stmt reads the data of an `ast.Stmt` node and returns a `DocNode`. // An option error is thrown if the symbol is not exposed to the public // (when `pub_only` is enabled) or the content's of the AST node is empty. pub fn (mut d Doc) stmt(stmt ast.Stmt, filename string) ?DocNode { mut name := d.stmt_name(stmt) if name in d.common_symbols { return error('already documented') } if name.starts_with(d.orig_mod_name + '.') { name = name.all_after(d.orig_mod_name + '.') } mut node := DocNode{ name: name content: d.stmt_signature(stmt) pos: stmt.pos file_path: os.join_path(d.base_path, filename) is_pub: d.stmt_pub(stmt) platform: platform_from_filename(filename) } if (!node.is_pub && d.pub_only) || stmt is ast.GlobalDecl { return error('symbol $node.name not public') } if node.name.starts_with(d.orig_mod_name + '.') { node.name = node.name.all_after(d.orig_mod_name + '.') } if node.name.len == 0 && node.comments.len == 0 && node.content.len == 0 { return error('empty stmt') } match stmt { ast.ConstDecl { node.kind = .const_group node.parent_name = 'Constants' if d.extract_vars { for field in stmt.fields { ret_type := if field.typ == 0 { d.expr_typ_to_string(field.expr) } else { d.type_to_str(field.typ) } node.children << DocNode{ name: field.name.all_after(d.orig_mod_name + '.') kind: .constant pos: field.pos return_type: ret_type } } } } ast.EnumDecl { node.kind = .enum_ if d.extract_vars { for field in stmt.fields { ret_type := if field.has_expr { d.expr_typ_to_string(field.expr) } else { 'int' } node.children << DocNode{ name: field.name kind: .enum_field parent_name: node.name pos: field.pos return_type: ret_type } } } } ast.InterfaceDecl { node.kind = .interface_ } ast.StructDecl { node.kind = .struct_ if d.extract_vars { for field in stmt.fields { ret_type := if field.typ == 0 && field.has_default_expr { d.expr_typ_to_string(field.default_expr) } else { d.type_to_str(field.typ) } node.children << DocNode{ name: field.name kind: .struct_field parent_name: node.name pos: field.pos return_type: ret_type } } } } ast.TypeDecl { node.kind = .typedef } ast.FnDecl { node.deprecated = stmt.is_deprecated node.kind = .function node.return_type = d.type_to_str(stmt.return_type) if stmt.receiver.typ !in [0, 1] { method_parent := d.type_to_str(stmt.receiver.typ) node.kind = .method node.parent_name = method_parent } if d.extract_vars { for param in stmt.params { node.children << DocNode{ name: param.name kind: .variable parent_name: node.name pos: param.pos attrs: map{ 'mut': param.is_mut.str() } return_type: d.type_to_str(param.typ) } } } } else { return error('invalid stmt type to document') } } included := node.name in d.filter_symbol_names || node.parent_name in d.filter_symbol_names if d.filter_symbol_names.len != 0 && !included { return error('not included in the list of symbol names') } if d.prefs.os == .all { d.common_symbols << node.name } return node } // file_ast reads the contents of `ast.File` and returns a map of `DocNode`s. pub fn (mut d Doc) file_ast(file_ast ast.File) map[string]DocNode { mut contents := map[string]DocNode{} stmts := file_ast.stmts d.fmt.file = file_ast d.fmt.set_current_module_name(d.orig_mod_name) d.fmt.process_file_imports(file_ast) mut last_import_stmt_idx := 0 for sidx, stmt in stmts { if stmt is ast.Import { last_import_stmt_idx = sidx } } mut preceeding_comments := []DocComment{} // mut imports_section := true for sidx, stmt in stmts { if stmt is ast.ExprStmt { // Collect comments if stmt.expr is ast.Comment { preceeding_comments << ast_comment_to_doc_comment(stmt.expr) continue } } // TODO: Fetch head comment once if stmt is ast.Module { if !d.with_head { continue } // the previous comments were probably a copyright/license one module_comment := merge_doc_comments(preceeding_comments) preceeding_comments = [] if !d.is_vlib && !module_comment.starts_with('Copyright (c)') { if module_comment == '' { continue } d.head.comments << preceeding_comments } continue } if last_import_stmt_idx > 0 && sidx == last_import_stmt_idx { // the accumulated comments were interspersed before/between the imports; // just add them all to the module comments: if d.with_head { d.head.comments << preceeding_comments } preceeding_comments = [] // imports_section = false } if stmt is ast.Import { continue } mut node := d.stmt(stmt, os.base(file_ast.path)) or { preceeding_comments = [] continue } if node.parent_name !in contents { parent_node_kind := if node.parent_name == 'Constants' { SymbolKind.const_group } else { SymbolKind.typedef } contents[node.parent_name] = DocNode{ name: node.parent_name kind: parent_node_kind } } if d.with_comments && (preceeding_comments.len > 0) { node.comments << preceeding_comments } preceeding_comments = [] if node.parent_name.len > 0 { parent_name := node.parent_name if node.parent_name == 'Constants' { node.parent_name = '' } contents[parent_name].children << node } else { contents[node.name] = node } } d.fmt.mod2alias = map[string]string{} if contents[''].kind != .const_group { contents.delete('') } return contents } // file_ast_with_pos has the same function as the `file_ast` but // instead returns a list of variables in a given offset-based position. pub fn (mut d Doc) file_ast_with_pos(file_ast ast.File, pos int) map[string]DocNode { lscope := file_ast.scope.innermost(pos) mut contents := map[string]DocNode{} for name, val in lscope.objects { if val !is ast.Var { continue } vr_data := val as ast.Var l_node := DocNode{ name: name pos: vr_data.pos file_path: file_ast.path from_scope: true kind: .variable return_type: d.expr_typ_to_string(vr_data.expr) } contents[l_node.name] = l_node } return contents } // generate is a `Doc` method that will start documentation // process based on a file path provided. pub fn (mut d Doc) generate() ? { // get all files d.base_path = if os.is_dir(d.base_path) { d.base_path } else { os.real_path(os.dir(d.base_path)) } d.is_vlib = !d.base_path.contains('vlib') project_files := os.ls(d.base_path) or { return err } v_files := d.prefs.should_compile_filtered_files(d.base_path, project_files) if v_files.len == 0 { return error_with_code('vdoc: No valid V files were found.', 1) } // parse files mut comments_mode := scanner.CommentsMode.skip_comments if d.with_comments { comments_mode = .toplevel_comments } global_scope := &ast.Scope{ parent: 0 } mut file_asts := []ast.File{} for i, file_path in v_files { if i == 0 { d.parent_mod_name = get_parent_mod(d.base_path) or { '' } } file_asts << parser.parse_file(file_path, d.table, comments_mode, d.prefs, global_scope) } return d.file_asts(file_asts) } // file_asts has the same function as the `file_ast` function but // accepts an array of `ast.File` and throws an error if necessary. pub fn (mut d Doc) file_asts(file_asts []ast.File) ? { mut fname_has_set := false d.orig_mod_name = file_asts[0].mod.name for i, file_ast in file_asts { if d.filename.len > 0 && file_ast.path.contains(d.filename) && !fname_has_set { d.filename = file_ast.path fname_has_set = true } if d.with_head && i == 0 { mut module_name := file_ast.mod.name // if module_name != 'main' && d.parent_mod_name.len > 0 { // module_name = d.parent_mod_name + '.' + module_name // } d.head = DocNode{ name: module_name content: 'module $module_name' kind: .none_ } } else if file_ast.mod.name != d.orig_mod_name { continue } if file_ast.path == d.filename { d.checker.check(file_ast) d.scoped_contents = d.file_ast_with_pos(file_ast, d.pos) } contents := d.file_ast(file_ast) for name, node in contents { if name !in d.contents { d.contents[name] = node continue } if d.contents[name].kind == .typedef && node.kind !in [.typedef, .none_] { old_children := d.contents[name].children.clone() d.contents[name] = node d.contents[name].children = old_children } if d.contents[name].kind != .none_ || node.kind == .none_ { d.contents[name].children << node.children d.contents[name].children.sort_by_name() d.contents[name].children.sort_by_kind() } } } if d.filter_symbol_names.len != 0 && d.contents.len != 0 { for filter_name in d.filter_symbol_names { if filter_name !in d.contents { return error('vdoc: `$filter_name` symbol in module `$d.orig_mod_name` not found') } } } d.time_generated = time.now() } // generate documents a certain file directory and returns an // instance of `Doc` if it is successful. Otherwise, it will throw an error. pub fn generate(input_path string, pub_only bool, with_comments bool, platform Platform, filter_symbol_names ...string) ?Doc { if platform == .js { return error('vdoc: Platform `$platform` is not supported.') } mut doc := new(input_path) doc.pub_only = pub_only doc.with_comments = with_comments doc.filter_symbol_names = filter_symbol_names.filter(it.len != 0) doc.prefs.os = if platform == .auto { pref.get_host_os() } else { pref.OS(int(platform)) } doc.generate() ? return doc } // generate_with_pos has the same function as the `generate` function but // accepts an offset-based position and enables the comments by default. pub fn generate_with_pos(input_path string, filename string, pos int) ?Doc { mut doc := new(input_path) doc.pub_only = false doc.with_comments = true doc.with_pos = true doc.filename = filename doc.pos = pos doc.generate() ? return doc }