From ad4f903b92ae6799e7089fce7dc4a9e12ee512a0 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 19 Jun 2026 23:31:01 +0300 Subject: [PATCH] cmd/tools/vdoc: fix `v vlib-docs` panic on modules without valid files for the platform (fix #27464) (#27503) --- cmd/tools/vdoc/vdoc.v | 34 +++++++++++++------- cmd/tools/vdoc/vdoc_test.v | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/cmd/tools/vdoc/vdoc.v b/cmd/tools/vdoc/vdoc.v index eddbb2873..9377d9a72 100644 --- a/cmd/tools/vdoc/vdoc.v +++ b/cmd/tools/vdoc/vdoc.v @@ -75,7 +75,7 @@ fn (vd &VDoc) gen_json(d doc.Doc) string { fn (mut vd VDoc) gen_plaintext(d doc.Doc) string { cfg := vd.cfg mut pw := strings.new_builder(200) - if cfg.is_color { + if cfg.is_color && d.head.content.contains(' ') { content_arr := d.head.content.split(' ') pw.writeln('${term.bright_blue(content_arr[0])} ${term.green(content_arr[1])}') } else { @@ -370,6 +370,13 @@ fn (mut vd VDoc) generate_docs_from_file() { exit(1) } } + if dcs.head.name == '' && dcs.contents.len == 0 { + // The folder had no valid V files for the target platform (e.g. the + // `ios`/`macos` modules when generating docs on Linux), so `generate` + // skipped it. There is nothing to document, so do not add an empty + // `Doc` that would later be rendered (and crash on its empty head). + continue + } if cfg.is_multi || (!cfg.is_multi && cfg.include_readme) { readme := vd.get_readme(dirpath) if readme.path != '' { @@ -397,22 +404,25 @@ fn (mut vd VDoc) generate_docs_from_file() { exit(1) } vd.vprintln('Rendering docs...') + if vd.docs.len == 0 { + // Every discovered module was skipped (e.g. a tree containing only files + // that are filtered out for the target platform), so there is nothing to + // render. Report it and fail, regardless of the output destination, instead + // of silently creating/cleaning an empty output directory and exiting 0. + if dirs.len == 0 { + eprintln('vdoc: No documentation found') + } else { + eprintln('vdoc: No documentation found for ${dirs[0]}') + } + exit(1) + } if out.path == '' || out.path == 'stdout' || out.path == '-' { if out.typ == .html { vd.render_static_html(out) } outputs := vd.render(out) - if outputs.len == 0 { - if dirs.len == 0 { - eprintln('vdoc: No documentation found') - } else { - eprintln('vdoc: No documentation found for ${dirs[0]}') - } - exit(1) - } else { - first := outputs.keys()[0] - println(outputs[first]) - } + first := outputs.keys()[0] + println(outputs[first]) } else { if !os.exists(out.path) { os.mkdir_all(out.path) or { panic(err) } diff --git a/cmd/tools/vdoc/vdoc_test.v b/cmd/tools/vdoc/vdoc_test.v index c952507e8..cf8f89cb0 100644 --- a/cmd/tools/vdoc/vdoc_test.v +++ b/cmd/tools/vdoc/vdoc_test.v @@ -317,3 +317,67 @@ fn test_markdown_renderer_preserves_wrapped_readme_markdown() ! { assert out.contains('the most simple token') assert out.contains('is not abc OR ebc') } + +fn test_doc_multi_skips_modules_without_valid_files_for_platform() { + // https://github.com/vlang/v/issues/27464 + // A module whose only V files are filtered out for the target platform (e.g. + // the `ios`/`macos` modules when generating docs on Linux) is skipped during + // generation. It must not produce an empty `Doc` that later crashes rendering. + base_dir := 'skip_platform_modules' + good_dir := os.join_path(base_dir, 'good') + skipped_dir := os.join_path(base_dir, 'winonly') + os.mkdir_all(good_dir)! + os.mkdir_all(skipped_dir)! + os.write_file(os.join_path(good_dir, 'good.v'), "module good + +// hello returns a greeting. +pub fn hello() string { + return 'hi' +} +")! + // This file only compiles on Windows, so on every other platform the `winonly` + // module has no valid V files and gets skipped. The test never runs on Windows + // (see the `vtest build: !windows` directive at the top of the file). + os.write_file(os.join_path(skipped_dir, 'winonly_windows.c.v'), 'module winonly + +// only_win does nothing useful here. +pub fn only_win() int { + return 1 +} +')! + // `-color` exercises the original crash path in `gen_plaintext`. + res := os.execute_opt('${vexe_} doc -no-timestamp -m -color -f text -o - ${os.quoted_path( + './' + base_dir)}') or { panic(err) } + // The crash showed up as a non-zero exit code (V panic), so this is the key check. + assert res.exit_code == 0 + assert res.output.contains('hello') + // The skipped module must not be documented (its public symbol must be absent). + // Note: `os.execute_opt` merges stderr, where the `Skipping folder: ...winonly` + // notice is printed, so we check for the rendered symbol rather than the name. + assert !res.output.contains('only_win') +} + +fn test_doc_multi_all_modules_skipped_fails_for_file_output() { + // https://github.com/vlang/v/issues/27464 (review follow-up) + // When every discovered module is filtered out for the target platform, there + // is nothing to document. Writing to a real output path must fail with the same + // `No documentation found` error as the stdout path, instead of silently + // creating/cleaning an empty output directory and exiting 0. + base_dir := 'all_skipped_modules' + skipped_dir := os.join_path(base_dir, 'winonly') + out_dir := os.join_path(base_dir, 'out') + os.mkdir_all(skipped_dir)! + os.write_file(os.join_path(skipped_dir, 'winonly_windows.c.v'), 'module winonly + +pub fn only_win() int { + return 1 +} +')! + // `os.execute` (not `execute_opt`) so the expected non-zero exit is not an error. + res := os.execute('${vexe_} doc -no-timestamp -m -f html -o ${os.quoted_path('./' + out_dir)} ${os.quoted_path( + './' + base_dir)}') + assert res.exit_code != 0 + assert res.output.contains('No documentation found') + // The output directory must not have been created. + assert !os.exists(out_dir) +} -- 2.39.5