520 lines
13 KiB
V
520 lines
13 KiB
V
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
|
|
}
|