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.table import v.util // intentionally in order as a guide when arranging the docnodes pub enum SymbolKind { none_ const_group constant variable function method interface_ typedef enum_ enum_field struct_ struct_field } pub struct Doc { prefs &pref.Preferences = new_vdoc_preferences() pub mut: base_path string table &table.Table = &table.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 // for storing the contents of the file. sources map[string]string parent_mod_name string orig_mod_name string extract_vars bool } pub struct DocPos { pub: line int col int len int } pub struct DocNode { pub mut: name string content string comment string pos DocPos = DocPos{-1, -1, 0} file_path string kind SymbolKind deprecated bool parent_name string return_type string children []DocNode attrs map[string]string from_scope bool } 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: return &pref.Preferences{ enable_globals: true } } pub fn new(input_path string) Doc { mut d := Doc{ base_path: os.real_path(input_path) table: table.new_table() head: DocNode{} contents: map[string]DocNode{} sources: map[string]string{} time_generated: time.now() } d.fmt = fmt.Fmt{ indent: 0 is_debug: false table: d.table } d.checker = checker.new_checker(d.table, d.prefs) return d } pub fn (mut d Doc) stmt(stmt ast.Stmt, filename string) ?DocNode { mut node := DocNode{ name: d.stmt_name(stmt) content: d.stmt_signature(stmt) comment: '' pos: d.convert_pos(filename, stmt.position()) file_path: os.join_path(d.base_path, filename) } if (!node.content.starts_with('pub') && d.pub_only) || stmt is ast.GlobalDecl { return error('symbol 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.comment.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: d.convert_pos(filename, 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: d.convert_pos(filename, 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: d.convert_pos(filename, 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: d.convert_pos(filename, param.pos) attrs: { 'mut': param.is_mut.str() } return_type: d.type_to_str(param.typ) } } } } else { return error('invalid stmt type to document') } } return node } 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 prev_comments := []ast.Comment{} mut imports_section := true for sidx, stmt in stmts { // eprintln('stmt typeof: ' + typeof(stmt)) if stmt is ast.ExprStmt { if stmt.expr is ast.Comment { prev_comments << 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 := get_comment_block_right_before(prev_comments) prev_comments = [] if !d.is_vlib && !module_comment.starts_with('Copyright (c)') { if module_comment in ['', d.head.comment] { continue } if d.head.comment != '' { d.head.comment += '\n' } d.head.comment += module_comment } 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 comment: if d.with_head { import_comments := merge_comments(prev_comments) if d.head.comment != '' { d.head.comment += '\n' } d.head.comment += import_comments } prev_comments = [] imports_section = false } if stmt is ast.Import { continue } mut node := d.stmt(stmt, os.base(file_ast.path)) or { prev_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 && (prev_comments.len > 0) { // last_comment := contents[contents.len - 1].comment // cmt := last_comment + '\n' + get_comment_block_right_before(prev_comments) mut cmt := get_comment_block_right_before(prev_comments) len := node.name.len // fixed-width symbol name at start of comment if cmt.starts_with(node.name) && cmt.len > len && cmt[len] == ` ` { cmt = '`${cmt[..len]}`' + cmt[len..] } node.comment = cmt } prev_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 } 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: d.convert_pos(os.base(file_ast.path), 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 } 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 = 'vlib' !in d.base_path project_files := os.ls(d.base_path) or { return error_with_code(err, 0) } 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 { '' } } filename := os.base(file_path) d.sources[filename] = util.read_file(file_path) or { '' } file_asts << parser.parse_file(file_path, d.table, comments_mode, d.prefs, global_scope) } return d.file_asts(file_asts) } 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 && d.filename in file_ast.path && !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].kind != .none_ || node.kind == .none_) { d.contents[name].children << node.children d.contents[name].children.sort_by_name() continue } d.contents[name] = node } } d.time_generated = time.now() } pub fn generate(input_path string, pub_only bool, with_comments bool) ?Doc { mut doc := new(input_path) doc.pub_only = pub_only doc.with_comments = with_comments doc.generate() ? return doc } 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 }