From 712b46864a9969e33d39fa3c34241deb753bd439 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:21:55 +0100 Subject: [PATCH] implement more code functionalities --- lib/core/code/README.md | 37 +++- lib/core/code/model_file.v | 120 ++++++++++- lib/core/code/model_file_test.v | 87 ++++++++ lib/core/code/model_module.v | 8 - lib/core/code/vlang_utils.v | 345 ++++++++++++++++++++++++++++++++ lib/mcp/vcode/vlang.v | 284 ++++++++++++++++++++++++++ 6 files changed, 871 insertions(+), 10 deletions(-) create mode 100644 lib/core/code/model_file_test.v create mode 100644 lib/core/code/vlang_utils.v create mode 100644 lib/mcp/vcode/vlang.v diff --git a/lib/core/code/README.md b/lib/core/code/README.md index 6bb1c215..cee3334d 100644 --- a/lib/core/code/README.md +++ b/lib/core/code/README.md @@ -35,4 +35,39 @@ for struct in structs { } ``` -The [openrpc/docgen](../openrpc/docgen/) module demonstrates a good use case, where codemodels are serialized into JSON schema's, to generate an OpenRPC description document from a client in v. \ No newline at end of file +The [openrpc/docgen](../openrpc/docgen/) module demonstrates a good use case, where codemodels are serialized into JSON schema's, to generate an OpenRPC description document from a client in v.## V Language Utilities + +The `vlang_utils.v` file provides a set of utility functions for working with V language files and code. These utilities are useful for: + +1. **File Operations** + - `list_v_files(dir string) ![]string` - Lists all V files in a directory, excluding generated files + - `get_module_dir(mod string) string` - Converts a V module path to a directory path + +2. **Code Inspection and Analysis** + - `get_function_from_file(file_path string, function_name string) !string` - Extracts a function definition from a file + - `get_function_from_module(module_path string, function_name string) !string` - Searches for a function across all files in a module + - `get_type_from_module(module_path string, type_name string) !string` - Searches for a type definition across all files in a module + +3. **V Language Tools** + - `vtest(fullpath string) !string` - Runs V tests on files or directories + - `vvet(fullpath string) !string` - Runs V vet on files or directories + +### Example Usage + +```v +// Find and extract a function definition +function_def := code.get_function_from_module('/path/to/module', 'my_function') or { + eprintln('Could not find function: ${err}') + return +} +println(function_def) + +// Run tests on a directory +test_results := code.vtest('/path/to/module') or { + eprintln('Tests failed: ${err}') + return +} +println(test_results) +``` + +These utilities are particularly useful when working with code generation, static analysis, or when building developer tools that need to inspect V code. diff --git a/lib/core/code/model_file.v b/lib/core/code/model_file.v index 64235922..28ed8da5 100644 --- a/lib/core/code/model_file.v +++ b/lib/core/code/model_file.v @@ -1,5 +1,6 @@ module code +import log import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.core.pathlib import os @@ -8,7 +9,6 @@ pub interface IFile { write(string, WriteOptions) ! write_str(WriteOptions) !string name string - write(string, WriteOptions) ! } pub struct File { @@ -132,6 +132,7 @@ pub fn (code VFile) write_str(options WriteOptions) !string { } pub fn (file VFile) get_function(name string) ?Function { + log.error('Looking for function ${name} in file ${file.name}') functions := file.items.filter(it is Function).map(it as Function) target_lst := functions.filter(it.name == name) @@ -161,3 +162,120 @@ pub fn (file VFile) functions() []Function { pub fn (file VFile) structs() []Struct { return file.items.filter(it is Struct).map(it as Struct) } + +// parse_vfile parses V code into a VFile struct +// It extracts the module name, imports, constants, structs, and functions +pub fn parse_vfile(code string) !VFile { + mut vfile := VFile{ + content: code + } + + lines := code.split_into_lines() + + // Extract module name + for line in lines { + trimmed := line.trim_space() + if trimmed.starts_with('module ') { + vfile.mod = trimmed.trim_string_left('module ').trim_space() + break + } + } + + // Extract imports + for line in lines { + trimmed := line.trim_space() + if trimmed.starts_with('import ') { + import_obj := parse_import(trimmed) + vfile.imports << import_obj + } + } + + // Extract constants + vfile.consts = parse_consts(code) or { []Const{} } + + // Split code into chunks for parsing structs and functions + mut chunks := []string{} + mut current_chunk := '' + mut brace_count := 0 + mut in_struct_or_fn := false + mut comment_block := []string{} + + for line in lines { + trimmed := line.trim_space() + + // Collect comments + if trimmed.starts_with('//') && !in_struct_or_fn { + comment_block << line + continue + } + + // Check for struct or function start + if (trimmed.starts_with('struct ') || trimmed.starts_with('pub struct ') || + trimmed.starts_with('fn ') || trimmed.starts_with('pub fn ')) && !in_struct_or_fn { + in_struct_or_fn = true + current_chunk = comment_block.join('\n') + if current_chunk != '' { + current_chunk += '\n' + } + current_chunk += line + comment_block = []string{} + + if line.contains('{') { + brace_count += line.count('{') + } + if line.contains('}') { + brace_count -= line.count('}') + } + + if brace_count == 0 { + // Single line definition + chunks << current_chunk + current_chunk = '' + in_struct_or_fn = false + } + continue + } + + // Add line to current chunk if we're inside a struct or function + if in_struct_or_fn { + current_chunk += '\n' + line + + if line.contains('{') { + brace_count += line.count('{') + } + if line.contains('}') { + brace_count -= line.count('}') + } + + // Check if we've reached the end of the struct or function + if brace_count == 0 { + chunks << current_chunk + current_chunk = '' + in_struct_or_fn = false + } + } + } + + // Parse each chunk and add to items + for chunk in chunks { + trimmed := chunk.trim_space() + + if trimmed.contains('struct ') || trimmed.contains('pub struct ') { + // Parse struct + struct_obj := parse_struct(chunk) or { + // Skip invalid structs + continue + } + vfile.items << struct_obj + } else if trimmed.contains('fn ') || trimmed.contains('pub fn ') { + // Parse function + fn_obj := parse_function(chunk) or { + // Skip invalid functions + continue + } + vfile.items << fn_obj + } + } + + return vfile +} diff --git a/lib/core/code/model_file_test.v b/lib/core/code/model_file_test.v new file mode 100644 index 00000000..ca421f4d --- /dev/null +++ b/lib/core/code/model_file_test.v @@ -0,0 +1,87 @@ +module code + +fn test_parse_vfile() { + code := ' +module test + +import os +import strings +import freeflowuniverse.herolib.core.texttools + +const ( + VERSION = \'1.0.0\' + DEBUG = true +) + +pub struct Person { +pub mut: + name string + age int +} + +// greet returns a greeting message +pub fn (p Person) greet() string { + return \'Hello, my name is \${p.name} and I am \${p.age} years old\' +} + +// create_person creates a new Person instance +pub fn create_person(name string, age int) Person { + return Person{ + name: name + age: age + } +} +' + + vfile := parse_vfile(code) or { + assert false, 'Failed to parse VFile: ${err}' + return + } + + // Test module name + assert vfile.mod == 'test' + + // Test imports + assert vfile.imports.len == 3 + assert vfile.imports[0].mod == 'os' + assert vfile.imports[1].mod == 'strings' + assert vfile.imports[2].mod == 'freeflowuniverse.herolib.core.texttools' + + // Test constants + assert vfile.consts.len == 2 + assert vfile.consts[0].name == 'VERSION' + assert vfile.consts[0].value == '\'1.0.0\'' + assert vfile.consts[1].name == 'DEBUG' + assert vfile.consts[1].value == 'true' + + // Test structs + structs := vfile.structs() + assert structs.len == 1 + assert structs[0].name == 'Person' + assert structs[0].is_pub == true + assert structs[0].fields.len == 2 + assert structs[0].fields[0].name == 'name' + assert structs[0].fields[0].typ.vgen() == 'string' + assert structs[0].fields[1].name == 'age' + assert structs[0].fields[1].typ.vgen() == 'int' + + // Test functions + functions := vfile.functions() + assert functions.len == 2 + + // Test method + assert functions[0].name == 'greet' + assert functions[0].is_pub == true + assert functions[0].receiver.typ.vgen() == 'Person' + assert functions[0].result.typ.vgen() == 'string' + + // Test standalone function + assert functions[1].name == 'create_person' + assert functions[1].is_pub == true + assert functions[1].params.len == 2 + assert functions[1].params[0].name == 'name' + assert functions[1].params[0].typ.vgen() == 'string' + assert functions[1].params[1].name == 'age' + assert functions[1].params[1].typ.vgen() == 'int' + assert functions[1].result.typ.vgen() == 'Person' +} diff --git a/lib/core/code/model_module.v b/lib/core/code/model_module.v index a920fed7..fcdc839d 100644 --- a/lib/core/code/model_module.v +++ b/lib/core/code/model_module.v @@ -43,35 +43,28 @@ pub fn (mod Module) write(path string, options WriteOptions) ! { } for file in mod.files { - console.print_debug('mod file write ${file.name}') file.write(module_dir.path, options)! } for folder in mod.folders { - console.print_debug('mod folder write ${folder.name}') folder.write('${path}/${mod.name}', options)! } for mod_ in mod.modules { - console.print_debug('mod write ${mod_.name}') mod_.write('${path}/${mod.name}', options)! } if options.format { - console.print_debug('format ${module_dir.path}') os.execute('v fmt -w ${module_dir.path}') } if options.compile { - console.print_debug('compile shared ${module_dir.path}') os.execute_opt('${pre} -shared ${module_dir.path}') or { log.fatal(err.msg()) } } if options.test { - console.print_debug('test ${module_dir.path}') os.execute_opt('${pre} test ${module_dir.path}') or { log.fatal(err.msg()) } } if options.document { docs_path := '${path}/${mod.name}/docs' - console.print_debug('document ${module_dir.path}') os.execute('v doc -f html -o ${docs_path} ${module_dir.path}') } @@ -82,7 +75,6 @@ pub fn (mod Module) write(path string, options WriteOptions) ! { pub fn (mod Module) write_str() !string { mut out := '' for file in mod.files { - console.print_debug("mod file write ${file.name}") out += file.write_str()! } diff --git a/lib/core/code/vlang_utils.v b/lib/core/code/vlang_utils.v new file mode 100644 index 00000000..a3e715ff --- /dev/null +++ b/lib/core/code/vlang_utils.v @@ -0,0 +1,345 @@ +module code + +import freeflowuniverse.herolib.mcp +import freeflowuniverse.herolib.mcp.logger +import os +import log + +// ===== FILE AND DIRECTORY OPERATIONS ===== + +// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v +// ARGS: +// dir string - directory path to search +// RETURNS: +// []string - list of absolute paths to V files +pub fn list_v_files(dir string) ![]string { + files := os.ls(dir) or { return error('Error listing directory: ${err}') } + + mut v_files := []string{} + for file in files { + if file.ends_with('.v') && !file.ends_with('_.v') { + filepath := os.join_path(dir, file) + v_files << filepath + } + } + + return v_files +} + +// get_module_dir converts a V module path to a directory path +// ARGS: +// mod string - module name (e.g., 'freeflowuniverse.herolib.mcp') +// RETURNS: +// string - absolute path to the module directory +pub fn get_module_dir(mod string) string { + module_parts := mod.trim_string_left('freeflowuniverse.herolib').split('.') + return '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/${module_parts.join('/')}' +} + +// ===== CODE PARSING UTILITIES ===== + +// find_closing_brace finds the position of the closing brace that matches an opening brace +// ARGS: +// content string - the string to search in +// start_i int - the position after the opening brace +// RETURNS: +// ?int - position of the matching closing brace, or none if not found +fn find_closing_brace(content string, start_i int) ?int { + mut brace_count := 1 + for i := start_i; i < content.len; i++ { + if content[i] == `{` { + brace_count++ + } else if content[i] == `}` { + brace_count-- + if brace_count == 0 { + return i + } + } + } + return none +} + +// get_function_from_file parses a V file and extracts a specific function block including its comments +// ARGS: +// file_path string - path to the V file +// function_name string - name of the function to extract +// RETURNS: +// string - the function block including comments, or error if not found +// pub fn get_function_from_file(file_path string, function_name string) !string { +// log.error('Looking for function ${function_name} in file ${file_path}') +// content := os.read_file(file_path) or { +// return error('Failed to read file: ${file_path}: ${err}') +// } + +// vfile := parse_vfile(content) or { +// return error('Failed to parse V file: ${file_path}: ${err}') +// } + +// for fn in vfile.functions() { +// if fn.name == function_name { +// return fn.code +// } +// } + +// return error('Function ${function_name} not found in ${file_path}') +// } + +// lines := content.split_into_lines() +// mut result := []string{} +// mut in_function := false +// mut brace_count := 0 +// mut comment_block := []string{} + +// for i, line in lines { +// trimmed := line.trim_space() + +// // Collect comments that might be above the function +// if trimmed.starts_with('//') { +// if !in_function { +// comment_block << line +// } else if brace_count > 0 { +// result << line +// } +// continue +// } + +// // Check if we found the function +// if !in_function && (trimmed.starts_with('fn ${function_name}(') +// || trimmed.starts_with('pub fn ${function_name}(')) { +// in_function = true +// // Add collected comments +// result << comment_block +// comment_block = [] +// result << line +// if line.contains('{') { +// brace_count++ +// } +// continue +// } + +// // If we're inside the function, keep track of braces +// if in_function { +// result << line + +// for c in line { +// if c == `{` { +// brace_count++ +// } else if c == `}` { +// brace_count-- +// } +// } + +// // If brace_count is 0, we've reached the end of the function +// if brace_count == 0 && trimmed.contains('}') { +// return result.join('\n') +// } +// } else { +// // Reset comment block if we pass a blank line +// if trimmed == '' { +// comment_block = [] +// } +// } +// } + +// if !in_function { +// return error('Function "${function_name}" not found in ${file_path}') +// } + +// return result.join('\n') +// } + +// get_function_from_module searches for a function in all V files within a module +// ARGS: +// module_path string - path to the module directory +// function_name string - name of the function to find +// RETURNS: +// string - the function definition if found, or error if not found +pub fn get_function_from_module(module_path string, function_name string) !Function { + v_files := list_v_files(module_path) or { + return error('Failed to list V files in ${module_path}: ${err}') + } + + log.error('Found ${v_files} V files in ${module_path}') + for v_file in v_files { + // Read the file content + content := os.read_file(v_file) or { + continue + } + + // Parse the file + vfile := parse_vfile(content) or { + continue + } + + // Look for the function + if fn_obj := vfile.get_function(function_name) { + return fn_obj + } + } + + return error('function ${function_name} not found in module ${module_path}') +} + +// get_type_from_module searches for a type definition in all V files within a module +// ARGS: +// module_path string - path to the module directory +// type_name string - name of the type to find +// RETURNS: +// string - the type definition if found, or error if not found +pub fn get_type_from_module(module_path string, type_name string) !string { + println('Looking for type ${type_name} in module ${module_path}') + v_files := list_v_files(module_path) or { + return error('Failed to list V files in ${module_path}: ${err}') + } + + for v_file in v_files { + println('Checking file: ${v_file}') + content := os.read_file(v_file) or { return error('Failed to read file ${v_file}: ${err}') } + + // Look for both regular and pub struct declarations + mut type_str := 'struct ${type_name} {' + mut i := content.index(type_str) or { -1 } + mut is_pub := false + + if i == -1 { + // Try with pub struct + type_str = 'pub struct ${type_name} {' + i = content.index(type_str) or { -1 } + is_pub = true + } + + if i == -1 { + type_import := content.split_into_lines().filter(it.contains('import') +&& it.contains(type_name)) + if type_import.len > 0 { + log.debug('debugzoooo') + mod := type_import[0].trim_space().trim_string_left('import ').all_before(' ') + return get_type_from_module(get_module_dir(mod), type_name) + } + continue + } + println('Found type ${type_name} in ${v_file} at position ${i}') + + // Find the start of the struct definition including comments + mut comment_start := i + mut line_start := i + + // Find the start of the line containing the struct definition + for j := i; j >= 0; j-- { + if j == 0 || content[j - 1] == `\n` { + line_start = j + break + } + } + + // Find the start of the comment block (if any) + for j := line_start - 1; j >= 0; j-- { + if j == 0 { + comment_start = 0 + break + } + + // If we hit a blank line or a non-comment line, stop + if content[j] == `\n` { + if j > 0 && j < content.len - 1 { + // Check if the next line starts with a comment + next_line_start := j + 1 + if next_line_start < content.len && content[next_line_start] != `/` { + comment_start = j + 1 + break + } + } + } + } + + // Find the end of the struct definition + closing_i := find_closing_brace(content, i + type_str.len) or { + return error('could not find where declaration for type ${type_name} ends') + } + + // Get the full struct definition including the struct declaration line + full_struct := content.substr(line_start, closing_i + 1) + println('Found struct definition:\n${full_struct}') + + // Return the full struct definition + return full_struct + } + + return error('type ${type_name} not found in module ${module_path}') +} + +// ===== V LANGUAGE TOOLS ===== + +// vtest runs v test on the specified file or directory +// ARGS: +// fullpath string - path to the file or directory to test +// RETURNS: +// string - test results output, or error if test fails +pub fn vtest(fullpath string) !string { + logger.info('test ${fullpath}') + if !os.exists(fullpath) { + return error('File or directory does not exist: ${fullpath}') + } + if os.is_dir(fullpath) { + mut results := '' + for item in list_v_files(fullpath)! { + results += vtest(item)! + results += '\n-----------------------\n' + } + return results + } else { + cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}' + logger.debug('Executing command: ${cmd}') + result := os.execute(cmd) + if result.exit_code != 0 { + return error('Test failed for ${fullpath} with exit code ${result.exit_code}\n${result.output}') + } else { + logger.info('Test completed for ${fullpath}') + } + return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}' + } +} + +// vet_file runs v vet on a single file +// ARGS: +// file string - path to the file to vet +// RETURNS: +// string - vet results output, or error if vet fails +fn vet_file(file string) !string { + cmd := 'v vet -v -w ${file}' + logger.debug('Executing command: ${cmd}') + result := os.execute(cmd) + if result.exit_code != 0 { + return error('Vet failed for ${file} with exit code ${result.exit_code}\n${result.output}') + } else { + logger.info('Vet completed for ${file}') + } + return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}' +} + +// vvet runs v vet on the specified file or directory +// ARGS: +// fullpath string - path to the file or directory to vet +// RETURNS: +// string - vet results output, or error if vet fails +pub fn vvet(fullpath string) !string { + logger.info('vet ${fullpath}') + if !os.exists(fullpath) { + return error('File or directory does not exist: ${fullpath}') + } + + if os.is_dir(fullpath) { + mut results := '' + files := list_v_files(fullpath) or { return error('Error listing V files: ${err}') } + for file in files { + results += vet_file(file) or { + logger.error('Failed to vet ${file}: ${err}') + return error('Failed to vet ${file}: ${err}') + } + results += '\n-----------------------\n' + } + return results + } else { + return vet_file(fullpath) + } +} diff --git a/lib/mcp/vcode/vlang.v b/lib/mcp/vcode/vlang.v new file mode 100644 index 00000000..0ac89da1 --- /dev/null +++ b/lib/mcp/vcode/vlang.v @@ -0,0 +1,284 @@ +module vcode + +import freeflowuniverse.herolib.mcp +import freeflowuniverse.herolib.mcp.logger +import os +import log + +fn get_module_dir(mod string) string { + module_parts := mod.trim_string_left('freeflowuniverse.herolib').split('.') + return '${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/${module_parts.join('/')}' +} + +// given a module path and a type name, returns the type definition of that type within that module +// for instance: get_type_from_module('lib/mcp/developer/vlang.v', 'Developer') might return struct Developer {...} +fn get_type_from_module(module_path string, type_name string) !string { + println('Looking for type ${type_name} in module ${module_path}') + v_files := list_v_files(module_path) or { + return error('Failed to list V files in ${module_path}: ${err}') + } + + for v_file in v_files { + println('Checking file: ${v_file}') + content := os.read_file(v_file) or { return error('Failed to read file ${v_file}: ${err}') } + + // Look for both regular and pub struct declarations + mut type_str := 'struct ${type_name} {' + mut i := content.index(type_str) or { -1 } + mut is_pub := false + + if i == -1 { + // Try with pub struct + type_str = 'pub struct ${type_name} {' + i = content.index(type_str) or { -1 } + is_pub = true + } + + if i == -1 { + type_import := content.split_into_lines().filter(it.contains('import') + && it.contains(type_name)) + if type_import.len > 0 { + log.debug('debugzoooo') + mod := type_import[0].trim_space().trim_string_left('import ').all_before(' ') + return get_type_from_module(get_module_dir(mod), type_name) + } + continue + } + println('Found type ${type_name} in ${v_file} at position ${i}') + + // Find the start of the struct definition including comments + mut comment_start := i + mut line_start := i + + // Find the start of the line containing the struct definition + for j := i; j >= 0; j-- { + if j == 0 || content[j - 1] == `\n` { + line_start = j + break + } + } + + // Find the start of the comment block (if any) + for j := line_start - 1; j >= 0; j-- { + if j == 0 { + comment_start = 0 + break + } + + // If we hit a blank line or a non-comment line, stop + if content[j] == `\n` { + if j > 0 && j < content.len - 1 { + // Check if the next line starts with a comment + next_line_start := j + 1 + if next_line_start < content.len && content[next_line_start] != `/` { + comment_start = j + 1 + break + } + } + } + } + + // Find the end of the struct definition + closing_i := find_closing_brace(content, i + type_str.len) or { + return error('could not find where declaration for type ${type_name} ends') + } + + // Get the full struct definition including the struct declaration line + full_struct := content.substr(line_start, closing_i + 1) + println('Found struct definition:\n${full_struct}') + + // Return the full struct definition + return full_struct + } + + return error('type ${type_name} not found in module ${module_path}') +} + +// given a module path and a function name, returns the function definition of that function within that module +// for instance: get_function_from_module('lib/mcp/developer/vlang.v', 'develop') might return fn develop(...) {...} +fn get_function_from_module(module_path string, function_name string) !string { + v_files := list_v_files(module_path) or { + return error('Failed to list V files in ${module_path}: ${err}') + } + + println('Found ${v_files.len} V files in ${module_path}') + for v_file in v_files { + println('Checking file: ${v_file}') + result := get_function_from_file(v_file, function_name) or { + println('Function not found in ${v_file}: ${err}') + continue + } + println('Found function ${function_name} in ${v_file}') + return result + } + + return error('function ${function_name} not found in module ${module_path}') +} + +fn find_closing_brace(content string, start_i int) ?int { + mut brace_count := 1 + for i := start_i; i < content.len; i++ { + if content[i] == `{` { + brace_count++ + } else if content[i] == `}` { + brace_count-- + if brace_count == 0 { + return i + } + } + } + return none +} + +// get_function_from_file parses a V file and extracts a specific function block including its comments +// ARGS: +// file_path string - path to the V file +// function_name string - name of the function to extract +// RETURNS: string - the function block including comments, or empty string if not found +fn get_function_from_file(file_path string, function_name string) !string { + content := os.read_file(file_path) or { + return error('Failed to read file: ${file_path}: ${err}') + } + + lines := content.split_into_lines() + mut result := []string{} + mut in_function := false + mut brace_count := 0 + mut comment_block := []string{} + + for i, line in lines { + trimmed := line.trim_space() + + // Collect comments that might be above the function + if trimmed.starts_with('//') { + if !in_function { + comment_block << line + } else if brace_count > 0 { + result << line + } + continue + } + + // Check if we found the function + if !in_function && (trimmed.starts_with('fn ${function_name}(') + || trimmed.starts_with('pub fn ${function_name}(')) { + in_function = true + // Add collected comments + result << comment_block + comment_block = [] + result << line + if line.contains('{') { + brace_count++ + } + continue + } + + // If we're inside the function, keep track of braces + if in_function { + result << line + + for c in line { + if c == `{` { + brace_count++ + } else if c == `}` { + brace_count-- + } + } + + // If brace_count is 0, we've reached the end of the function + if brace_count == 0 && trimmed.contains('}') { + return result.join('\n') + } + } else { + // Reset comment block if we pass a blank line + if trimmed == '' { + comment_block = [] + } + } + } + + if !in_function { + return error('Function "${function_name}" not found in ${file_path}') + } + + return result.join('\n') +} + +// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v +fn list_v_files(dir string) ![]string { + files := os.ls(dir) or { return error('Error listing directory: ${err}') } + + mut v_files := []string{} + for file in files { + if file.ends_with('.v') && !file.ends_with('_.v') { + filepath := os.join_path(dir, file) + v_files << filepath + } + } + + return v_files +} + +// test runs v test on the specified file or directory +pub fn vtest(fullpath string) !string { + logger.info('test ${fullpath}') + if !os.exists(fullpath) { + return error('File or directory does not exist: ${fullpath}') + } + if os.is_dir(fullpath) { + mut results := '' + for item in list_v_files(fullpath)! { + results += vtest(item)! + results += '\n-----------------------\n' + } + return results + } else { + cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}' + logger.debug('Executing command: ${cmd}') + result := os.execute(cmd) + if result.exit_code != 0 { + return error('Test failed for ${fullpath} with exit code ${result.exit_code}\n${result.output}') + } else { + logger.info('Test completed for ${fullpath}') + } + return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}' + } +} + +// vvet runs v vet on the specified file or directory +pub fn vvet(fullpath string) !string { + logger.info('vet ${fullpath}') + if !os.exists(fullpath) { + return error('File or directory does not exist: ${fullpath}') + } + + if os.is_dir(fullpath) { + mut results := '' + files := list_v_files(fullpath) or { return error('Error listing V files: ${err}') } + for file in files { + results += vet_file(file) or { + logger.error('Failed to vet ${file}: ${err}') + return error('Failed to vet ${file}: ${err}') + } + results += '\n-----------------------\n' + } + return results + } else { + return vet_file(fullpath) + } +} + +// vet_file runs v vet on a single file +fn vet_file(file string) !string { + cmd := 'v vet -v -w ${file}' + logger.debug('Executing command: ${cmd}') + result := os.execute(cmd) + if result.exit_code != 0 { + return error('Vet failed for ${file} with exit code ${result.exit_code}\n${result.output}') + } else { + logger.info('Vet completed for ${file}') + } + return 'Command: ${cmd}\nExit code: ${result.exit_code}\nOutput:\n${result.output}' +} + +// cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}'