vdoc: markdown, html, json generation + lots of fixes

pull/5173/head
Ned Palacios 2020-06-02 18:10:01 +08:00 committed by GitHub
parent 46dbbd0ed0
commit b99ba21ddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 772 additions and 153 deletions

472
cmd/tools/vdoc.v 100644

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,7 @@ V supports the following commands:
test Run all test files in the provided directory. test Run all test files in the provided directory.
fmt Format the V code provided. fmt Format the V code provided.
doc Generate the documentation for a V module. doc Generate the documentation for a V module.
vlib-docs Generate and open the documentation of all the vlib modules.
repl Run the REPL. repl Run the REPL.
* Installation/self updating: * Installation/self updating:
symlink Create a symbolic link for V. symlink Create a symbolic link for V.
@ -37,4 +38,4 @@ Use "v help <command>" for more information about a command, example: `v help bu
Use "v help other" to see less frequently used commands. Use "v help other" to see less frequently used commands.
Note: Help is required to write more help topics. Note: Help is required to write more help topics.
Only build, fmt, run, test, search, install, remove, update, bin2v are properly documented currently. Only build, doc, fmt, run, test, search, install, remove, update, bin2v are properly documented currently.

16
cmd/v/help/doc.txt 100644
View File

@ -0,0 +1,16 @@
Usage:
v doc [flags] module_name output_file[.json|.md|.html]
v doc [flags] path_to_source.v output_file[.json|.md|.html]
v doc [flags] ./directory output_file[.json|.md|.html]
v doc [flags] . output_file[.json|.md|.html]
Generates the documentation of a given directory, module, or V source file
and prints or saves them to its desired format. It can generate HTML, JSON,
or Markdown format.
Options:
-all Includes private and public functions/methods/structs/consts/enums.
-loc Show the locations of the generated signatures. (For plaintext only)
-m Generate docs for modules listed in that folder.
-s Serve HTML-generated docs via HTTP.
-r Include README.md to docs if present.

View File

@ -5,8 +5,6 @@ module main
import help import help
import os import os
import v.table
import v.doc
import v.pref import v.pref
import v.util import v.util
import v.builder import v.builder
@ -19,7 +17,7 @@ const (
'repl', 'repl',
'build-tools', 'build-examples', 'build-tools', 'build-examples',
'build-vbinaries', 'build-vbinaries',
'setup-freetype' 'setup-freetype', 'doc'
] ]
list_of_flags_that_allow_duplicates = ['cc', 'd', 'define', 'cf', 'cflags'] list_of_flags_that_allow_duplicates = ['cc', 'd', 'define', 'cf', 'cflags']
) )
@ -80,6 +78,9 @@ fn main_v() {
util.launch_tool(prefs.is_verbose, 'vpm', os.args[1..]) util.launch_tool(prefs.is_verbose, 'vpm', os.args[1..])
return return
} }
'vlib-docs' {
util.launch_tool(prefs.is_verbose, 'vdoc', ['doc', '-m', '-s', '-r', os.join_path(os.base_dir(@VEXE), 'vlib')])
}
'get' { 'get' {
println('V Error: Use `v install` to install modules from vpm.vlang.io') println('V Error: Use `v install` to install modules from vpm.vlang.io')
exit(1) exit(1)
@ -88,17 +89,6 @@ fn main_v() {
println(util.full_v_version(prefs.is_verbose)) println(util.full_v_version(prefs.is_verbose))
return return
} }
'doc' {
mut mod := ''
if args.len == 1 {
println('v doc [module]')
} else if args.len > 1 {
mod = args[1]
}
table := table.new_table()
println(doc.doc(mod, table, prefs))
return
}
else {} else {}
} }
if command in ['run', 'build-module'] || command.ends_with('.v') || os.exists(command) { if command in ['run', 'build-module'] || command.ends_with('.v') || os.exists(command) {

View File

@ -1,149 +1,289 @@
module doc module doc
import strings
import v.pref
import v.table
import v.parser
import v.ast
import os import os
import strings
import time
import v.ast
import v.fmt
import v.parser
import v.pref
import v.scanner
import v.table
import v.token
import v.util
struct Doc { pub struct Doc {
table &table.Table pub mut:
mod string input_path string = ''
mut: prefs &pref.Preferences = &pref.Preferences{}
out strings.Builder table &table.Table = &table.Table{}
stmts []ast.Stmt // all module statements from all files pub_only bool = true
head DocNode
with_comments bool = true
contents []DocNode
time_generated time.Time
} }
type FilterFn = fn (node ast.FnDecl) bool pub struct DocPos {
pub:
line int
col int
}
pub fn doc(mod string, table &table.Table, prefs &pref.Preferences) string { pub struct DocNode {
mut d := Doc{ pub mut:
out: strings.new_builder(1000) name string
table: table content string = ''
mod: mod comment string
pos DocPos = DocPos{-1,-1}
file_path string = ''
parent_type string = ''
}
pub fn write_comment_bw(stmts []ast.Stmt, start_idx int) string {
mut comment := ''
for i := start_idx; i >= 0; i-- {
stmt := stmts[i]
if stmt is ast.Comment {
cmt := stmt as ast.Comment
cmt_content := cmt.text.trim_left('|')
comment = cmt_content + if cmt_content.starts_with('```') { '\n' } else { ' ' } + comment
} else {
panic('Not a comment')
}
if i-1 >= 0 && !(stmts[i-1] is ast.Comment) {
break
}
} }
vlib_path := os.dir(pref.vexe_path()) + '/vlib'
mod_path := mod.replace('.', os.path_separator) return comment
path := os.join_path(vlib_path, mod_path) }
if mod == '' || !os.exists(path) {
if mod != '' { fn convert_pos(file_path string, pos token.Position) DocPos {
println('module "$mod" not found') source := util.read_file(file_path) or {''}
mut p := util.imax(0, util.imin(source.len - 1, pos.pos))
column := util.imax(0, pos.pos - p - 1)
return DocPos{ line: pos.line_nr+1, col: util.imax(1, column+1) }
}
pub fn (d Doc) get_signature(stmt ast.Stmt) string {
mut f := fmt.Fmt{
out: strings.new_builder(1000)
out_imports: strings.new_builder(200)
table: d.table
indent: 0
is_debug: false
}
match stmt {
ast.Module {
return 'module $it.name'
} }
println('\navailable modules:') ast.FnDecl {
mut files := os.ls(vlib_path) or { return it.str(d.table)
return ''
} }
files.sort() else {
for file in files { f.stmt(stmt)
println(file) return f.out.str().trim_space()
} }
// println(path) }
}
pub fn (d Doc) get_pos(stmt ast.Stmt) token.Position {
match stmt {
ast.FnDecl { return it.pos }
ast.StructDecl { return it.pos }
ast.EnumDecl { return it.pos }
ast.InterfaceDecl { return it.pos }
ast.ConstDecl { return it.pos }
else { return token.Position{} }
}
}
pub fn (d Doc) get_name(stmt ast.Stmt) string {
match stmt {
ast.FnDecl { return it.name }
ast.StructDecl { return it.name }
ast.EnumDecl { return it.name }
ast.InterfaceDecl { return it.name }
ast.ConstDecl { return 'Constants' }
else { return '' }
}
}
pub fn new(input_path string) Doc {
return Doc{
input_path: os.real_path(input_path),
prefs: &pref.Preferences{},
table: table.new_table(),
head: DocNode{},
contents: []DocNode{},
time_generated: time.now()
}
}
pub fn (nodes []DocNode) find_children_of(parent_type string) []DocNode {
if parent_type.len == 0 { return []DocNode{} }
mut children := []DocNode{}
for node in nodes {
if node.parent_type != parent_type { continue }
children << node
}
return children
}
fn get_parent_mod(dir string) ?string {
base_dir := os.base_dir(dir)
if os.file_name(base_dir) in ['encoding', 'v'] && 'vlib' in base_dir {
return os.file_name(base_dir)
}
prefs := &pref.Preferences{}
files := os.ls(base_dir) or { []string{} }
v_files := prefs.should_compile_filtered_files(base_dir, files)
if v_files.len == 0 {
parent_mod := get_parent_mod(base_dir) or { '' }
if parent_mod.len > 0 {
return parent_mod + '.' + os.file_name(base_dir)
}
return error('No V files found.')
}
file_ast := parser.parse_file(
v_files[0],
table.new_table(),
.skip_comments,
prefs,
&ast.Scope{parent: 0}
)
if file_ast.mod.name == 'main' {
return '' return ''
} }
// vfiles := os.walk_ext(path, '.v') parent_mod := get_parent_mod(base_dir) or { '' }
files := os.ls(path) or { if parent_mod.len > 0 {
panic(err) return parent_mod + '.' + file_ast.mod.name
} }
filtered_files := prefs.should_compile_filtered_files(path, files) return file_ast.mod.name
for file in filtered_files {
fscope := &ast.Scope{
parent: 0
}
file_ast := parser.parse_file(file, table, .skip_comments, prefs, fscope)
d.stmts << file_ast.stmts
}
if d.stmts.len == 0 {
println('nothing here')
exit(1)
}
d.print_structs()
d.print_enums()
d.print_fns()
d.out.writeln('')
d.print_methods()
/*
for stmt in file_ast.stmts {
d.stmt(stmt)
}
println(path)
*/
return d.out.str().trim_space()
} }
fn (d &Doc) get_fn_node(f ast.FnDecl) string { pub fn (mut d Doc) generate() ?bool {
return f.str(d.table).replace_each([d.mod + '.', '', 'pub ', '']) // get all files
} base_path := if os.is_dir(d.input_path) { d.input_path } else { os.real_path(os.base_dir(d.input_path)) }
project_files := os.ls(base_path) or { panic(err) }
v_files := d.prefs.should_compile_filtered_files(base_path, project_files)
fn (mut d Doc) print_fns() { if v_files.len == 0 {
fn_signatures := d.get_fn_signatures(is_pub_function) return error('vdoc: No valid V files were found.')
d.write_fn_signatures(fn_signatures)
}
fn (mut d Doc) print_methods() {
fn_signatures := d.get_fn_signatures(is_pub_method)
d.write_fn_signatures(fn_signatures)
}
[inline]
fn (mut d Doc) write_fn_signatures(fn_signatures []string) {
for s in fn_signatures {
d.out.writeln(s)
} }
} // parse files
mut file_asts := []ast.File{}
// TODO: remove later for vlib
comments_mode := if d.with_comments {
scanner.CommentsMode.parse_comments
} else {
scanner.CommentsMode.skip_comments
}
for file in v_files {
file_ast := parser.parse_file(
file,
d.table,
comments_mode,
d.prefs,
&ast.Scope{parent: 0}
)
fn (d Doc) get_fn_signatures(filter_fn FilterFn) []string { file_asts << file_ast
mut fn_signatures := []string{} }
for stmt in d.stmts {
match stmt { mut module_name := ''
ast.FnDecl { mut parent_mod_name := ''
if filter_fn(it) { mut orig_mod_name := ''
fn_signatures << d.get_fn_node(it)
} for i, file_ast in file_asts {
if i == 0 {
parent_mod_name = get_parent_mod(base_path) or { '' }
module_name = file_ast.mod.name
orig_mod_name = module_name
if module_name != 'main' && parent_mod_name.len > 0 {
module_name = parent_mod_name + '.' + module_name
} }
else {} d.head = DocNode{
} name: module_name,
} content: 'module $module_name',
fn_signatures.sort() comment: ''
return fn_signatures }
} } else if file_ast.mod.name != module_name {
fn is_pub_method(node ast.FnDecl) bool {
return node.is_pub && node.is_method && !node.is_deprecated
}
fn is_pub_function(node ast.FnDecl) bool {
return node.is_pub && !node.is_method && !node.is_deprecated
}
// TODO it's probably better to keep using AST, not `table`
fn (mut d Doc) print_enums() {
for typ in d.table.types {
if typ.kind != .enum_ {
continue continue
} }
d.out.writeln('enum $typ.name {')
info := typ.info as table.Enum stmts := file_ast.stmts
for val in info.vals { for si, stmt in stmts {
d.out.writeln('\t$val') if stmt is ast.Comment { continue }
if !(stmt is ast.Module) {
// todo: accumulate consts
mut name := d.get_name(stmt)
signature := d.get_signature(stmt)
pos := d.get_pos(stmt)
if !signature.starts_with('pub') && d.pub_only {
continue
}
if name.starts_with(orig_mod_name + '.') {
name = name.all_after(orig_mod_name + '.')
}
mut node := DocNode{
name: name,
content: signature,
comment: '',
pos: convert_pos(v_files[i], pos),
file_path: v_files[i]
}
if stmt is ast.FnDecl {
fnd := stmt as ast.FnDecl
if fnd.receiver.typ != 0 {
mut parent_type := d.table.get_type_name(fnd.receiver.typ)
if parent_type.starts_with(module_name + '.') {
parent_type = parent_type.all_after(module_name + '.')
}
node.parent_type = parent_type
}
}
d.contents << node
}
if d.with_comments && (si-1 >= 0 && stmts[si-1] is ast.Comment) {
if stmt is ast.Module {
d.head.comment = write_comment_bw(stmts, si-1)
} else {
last_comment := d.contents[d.contents.len-1].comment
d.contents[d.contents.len-1].comment = last_comment + '\n' + write_comment_bw(stmts, si-1)
}
}
} }
d.out.writeln('}')
} }
d.time_generated = time.now()
return true
} }
fn (mut d Doc) print_structs() { pub fn generate(input_path string, pub_only bool, with_comments bool) ?Doc {
for typ in d.table.types { mut doc := doc.new(input_path)
if typ.kind != .struct_ || !typ.name.starts_with(d.mod + '.') { doc.pub_only = pub_only
// !typ.name[0].is_capital() || typ.name.starts_with('C.') { doc.with_comments = with_comments
continue
} _ = doc.generate() or {
name := typ.name.after('.') return error(err)
d.out.writeln('struct $name {')
info := typ.info as table.Struct
for field in info.fields {
sym := d.table.get_type_symbol(field.typ)
d.out.writeln('\t$field.name $sym.name')
}
d.out.writeln('}\n')
} }
}
return doc
}

View File

@ -2,9 +2,9 @@ import v.table
import v.doc import v.doc
import v.pref import v.pref
fn test_vdoc() { // fn test_vdoc() {
mut prefs := &pref.Preferences{} // mut prefs := &pref.Preferences{}
prefs.fill_with_defaults() // prefs.fill_with_defaults()
table := table.new_table() // table := table.new_table()
println(doc.doc('net', table, prefs)) // println(doc.doc('net', table, prefs))
} // }

View File

@ -316,14 +316,14 @@ and the existing module `${modulename}` may still work.')
} }
pub fn ensure_modules_for_all_tools_are_installed(is_verbose bool) { pub fn ensure_modules_for_all_tools_are_installed(is_verbose bool) {
for tool_name, tool_modules in external_module_dependencies_for_tool { for tool_name, tool_modules in external_module_dependencies_for_tool {
if is_verbose { if is_verbose {
eprintln('Installing modules for tool: $tool_name ...') eprintln('Installing modules for tool: $tool_name ...')
} }
for emodule in tool_modules { for emodule in tool_modules {
util.check_module_is_installed(emodule, is_verbose) or { util.check_module_is_installed(emodule, is_verbose) or {
panic(err) panic(err)
} }
} }
} }
} }