main.ha (11440B)
1 // SPDX-License-Identifier: GPL-3.0-only 2 // (c) Hare authors <https://harelang.org> 3 4 use bufio; 5 use cmd::haredoc::doc; 6 use fmt; 7 use fs; 8 use getopt; 9 use hare::ast; 10 use hare::lex; 11 use hare::module; 12 use hare::parse; 13 use hare::unparse; 14 use io; 15 use memio; 16 use os; 17 use os::exec; 18 use path; 19 use strconv; 20 use strings; 21 22 const help: []getopt::help = [ 23 "reads and formats Hare documentation", 24 ('a', "show undocumented members (only applies to -Ftty)"), 25 ('t', "disable HTML template (requires postprocessing)"), 26 ('F', "format", "specify output format (tty or html)"), 27 ('T', "tagset", "set/unset build tags"), 28 "[identifier|path]", 29 ]; 30 31 export fn main() void = { 32 const cmd = getopt::parse(os::args, help...); 33 defer getopt::finish(&cmd); 34 match (doc(os::args[0], &cmd)) { 35 case void => void; 36 case let e: doc::error => 37 fmt::fatal(doc::strerror(e)); 38 case let e: exec::error => 39 fmt::fatal(exec::strerror(e)); 40 case let e: fs::error => 41 fmt::fatal(fs::strerror(e)); 42 case let e: io::error => 43 fmt::fatal(io::strerror(e)); 44 case let e: module::error => 45 fmt::fatal(module::strerror(e)); 46 case let e: path::error => 47 fmt::fatal(path::strerror(e)); 48 case let e: parse::error => 49 fmt::fatal(parse::strerror(e)); 50 case let e: strconv::error => 51 fmt::fatal(strconv::strerror(e)); 52 }; 53 }; 54 55 fn doc(name: str, cmd: *getopt::command) (void | error) = { 56 let html = false; 57 let template = true; 58 let show_undocumented = false; 59 let tags: []str = default_tags(); 60 defer free(tags); 61 62 for (let (k, v) .. cmd.opts) { 63 switch (k) { 64 case 'F' => 65 switch (v) { 66 case "tty" => 67 html = false; 68 case "html" => 69 html = true; 70 case => 71 fmt::fatal("Invalid format", v); 72 }; 73 case 'T' => 74 merge_tags(&tags, v)?; 75 case 't' => 76 template = false; 77 case 'a' => 78 show_undocumented = true; 79 case => abort(); 80 }; 81 }; 82 83 if (show_undocumented && html) { 84 fmt::fatal("Option -a must be used only with -Ftty"); 85 }; 86 87 if (len(cmd.args) > 1) { 88 getopt::printusage(os::stderr, os::args[0], help)!; 89 os::exit(os::status::FAILURE); 90 }; 91 92 let ctx = module::context { 93 harepath = harepath(), 94 harecache = harecache(), 95 tags = tags, 96 }; 97 98 let declpath = ""; 99 defer free(declpath); 100 let declsrcs = module::srcset { ... }; 101 defer module::finish_srcset(&declsrcs); 102 let modpath = ""; 103 defer free(modpath); 104 let modsrcs = module::srcset { ... }; 105 defer module::finish_srcset(&modsrcs); 106 let id: ast::ident = []; 107 defer free(id); 108 109 if (len(cmd.args) == 0) { 110 let (p, s) = module::find(&ctx, []: ast::ident)?; 111 modpath = strings::dup(p); 112 modsrcs = s; 113 } else match (parseident(cmd.args[0])) { 114 case let ident: (ast::ident, bool) => 115 id = ident.0; 116 const trailing = ident.1; 117 if (!trailing) { 118 // check if it's an ident inside a module 119 match (module::find(&ctx, id[..len(id)-1])) { 120 case let s: (str, module::srcset) => 121 declpath = strings::dup(s.0); 122 declsrcs = s.1; 123 case let e: module::error => 124 module::finish_error(e); 125 }; 126 }; 127 // check if it's a module 128 match (module::find(&ctx, id)) { 129 case let s: (str, module::srcset) => 130 modpath = strings::dup(s.0); 131 modsrcs = s.1; 132 case let e: module::error => 133 module::finish_error(e); 134 if (declpath == "") { 135 const id = unparse::identstr(id); 136 fmt::fatalf("Could not find {}{}", id, 137 if (trailing) "::" else ""); 138 }; 139 }; 140 case void => 141 let buf = path::init(cmd.args[0])?; 142 let (p, s) = module::find(&ctx, &buf)?; 143 modpath = strings::dup(p); 144 modsrcs = s; 145 }; 146 147 let decls: []ast::decl = []; 148 defer { 149 for (let decl .. decls) { 150 ast::decl_finish(decl); 151 }; 152 free(decls); 153 }; 154 155 if (declpath != "") { 156 for (let ha .. declsrcs.ha) { 157 let u = doc::scan(ha)?; 158 ast::imports_finish(u.imports); 159 append(decls, u.decls...); 160 }; 161 162 let matching: []ast::decl = []; 163 let notmatching: []ast::decl = []; 164 165 for (let decl .. decls) { 166 if (has_decl(decl, id[len(id) - 1])) { 167 append(matching, decl); 168 } else { 169 append(notmatching, decl); 170 }; 171 }; 172 get_init_funcs(&matching, ¬matching); 173 174 for (let decl .. notmatching) { 175 ast::decl_finish(decl); 176 }; 177 free(notmatching); 178 free(decls); 179 decls = matching; 180 181 if (len(matching) == 0) { 182 if (modpath == "") { 183 const id = unparse::identstr(id); 184 fmt::fatalf("Could not find {}", id); 185 }; 186 } else { 187 show_undocumented = true; 188 }; 189 }; 190 191 let readme: (io::file | void) = void; 192 defer match (readme) { 193 case void => void; 194 case let f: io::file => 195 io::close(f)!; 196 }; 197 198 const ambiguous = modpath != "" && len(decls) > 0; 199 200 if (len(decls) == 0) { 201 for (let ha .. modsrcs.ha) { 202 let u = doc::scan(ha)?; 203 ast::imports_finish(u.imports); 204 append(decls, u.decls...); 205 }; 206 207 const rpath = path::init(modpath, "README")!; 208 match (os::open(path::string(&rpath))) { 209 case let f: io::file => 210 readme = f; 211 case fs::error => void; 212 }; 213 }; 214 215 const submods: []str = if (!ambiguous && modpath != "") { 216 yield match (doc::submodules(modpath, show_undocumented)) { 217 case let s: []str => 218 yield s; 219 case doc::error => 220 yield []; 221 }; 222 } else []; 223 const srcs = if (!ambiguous && modpath != "") modsrcs else declsrcs; 224 const summary = doc::sort_decls(decls); 225 defer doc::finish_summary(summary); 226 const ctx = doc::context { 227 mctx = &ctx, 228 ident = id, 229 tags = tags, 230 ambiguous = ambiguous, 231 srcs = srcs, 232 submods = submods, 233 summary = summary, 234 template = template, 235 readme = readme, 236 show_undocumented = show_undocumented, 237 out = os::stdout, 238 pager = void, 239 }; 240 241 const ret = if (html) { 242 yield doc::emit_html(&ctx); 243 } else { 244 ctx.out = init_tty(&ctx); 245 yield doc::emit_tty(&ctx); 246 }; 247 248 io::close(ctx.out)!; 249 match (ctx.pager) { 250 case void => void; 251 case let proc: exec::process => 252 exec::wait(&proc)!; 253 }; 254 255 // TODO: remove ? (harec bug workaround) 256 return ret?; 257 }; 258 259 // Nearly identical to parse::identstr, except alphanumeric lexical tokens are 260 // converted to strings and there must be no trailing tokens that don't belong 261 // to the ident in the string. For example, this function will parse `rt::abort` 262 // as a valid identifier. 263 fn parseident(in: str) ((ast::ident, bool) | void) = { 264 let buf = memio::fixed(strings::toutf8(in)); 265 let sc = bufio::newscanner(&buf); 266 defer bufio::finish(&sc); 267 let lexer = lex::init(&sc, "<string>"); 268 let success = false; 269 let ident: ast::ident = []; 270 defer if (!success) ast::ident_free(ident); 271 let trailing = false; 272 let z = 0z; 273 for (true) { 274 const tok = lex::lex(&lexer)!; 275 const name = if (tok.0 == lex::ltok::NAME) { 276 yield tok.1 as str; 277 } else if (tok.0 < lex::ltok::LAST_KEYWORD) { 278 yield strings::dup(lex::tokstr(tok)); 279 } else if (tok.0 == lex::ltok::EOF && len(ident) > 0) { 280 trailing = true; 281 break; 282 } else { 283 lex::unlex(&lexer, tok); 284 return; 285 }; 286 append(ident, name); 287 z += len(name); 288 const tok = lex::lex(&lexer)!; 289 switch (tok.0) { 290 case lex::ltok::EOF => 291 break; 292 case lex::ltok::DOUBLE_COLON => 293 z += 1; 294 case => 295 lex::unlex(&lexer, tok); 296 return; 297 }; 298 }; 299 if (z > ast::IDENT_MAX) { 300 return; 301 }; 302 success = true; 303 return (ident, trailing); 304 }; 305 306 fn init_tty(ctx: *doc::context) io::handle = { 307 const pager = match (os::getenv("PAGER")) { 308 case let name: str => 309 yield match (exec::cmd(name)) { 310 case let cmd: exec::command => 311 yield cmd; 312 case exec::error => 313 return os::stdout; 314 }; 315 case void => 316 yield match (exec::cmd("less", "-R")) { 317 case let cmd: exec::command => 318 yield cmd; 319 case exec::error => 320 yield match (exec::cmd("more", "-R")) { 321 case let cmd: exec::command => 322 yield cmd; 323 case exec::error => 324 return os::stdout; 325 }; 326 }; 327 }; 328 329 const pipe = exec::pipe(); 330 defer io::close(pipe.0)!; 331 exec::addfile(&pager, os::stdin_file, pipe.0); 332 // Get raw flag in if possible 333 exec::setenv(&pager, "LESS", os::tryenv("LESS", "FRX"))!; 334 exec::setenv(&pager, "MORE", os::tryenv("MORE", "R"))!; 335 ctx.pager = exec::start(&pager)!; 336 return pipe.1; 337 }; 338 339 fn has_decl(decl: ast::decl, name: str) bool = { 340 if (!decl.exported) { 341 return false; 342 }; 343 344 match (decl.decl) { 345 case let consts: []ast::decl_const => 346 for (let d .. consts) { 347 if (len(d.ident) == 1 && d.ident[0] == name) { 348 return true; 349 }; 350 }; 351 case let d: ast::decl_func => 352 if (len(d.ident) == 1 && d.ident[0] == name) { 353 return true; 354 }; 355 let tok = strings::rtokenize(d.symbol, "."); 356 match (strings::next_token(&tok)) { 357 case done => void; 358 case let s: str => 359 return s == name; 360 }; 361 case let globals: []ast::decl_global => 362 for (let d .. globals) { 363 if (len(d.ident) == 1 && d.ident[0] == name) { 364 return true; 365 }; 366 let tok = strings::rtokenize(d.symbol, "."); 367 match (strings::next_token(&tok)) { 368 case done => void; 369 case let s: str => 370 return s == name; 371 }; 372 }; 373 case let types: []ast::decl_type => 374 for (let d .. types) { 375 if (len(d.ident) == 1 && d.ident[0] == name) { 376 return true; 377 }; 378 }; 379 case ast::assert_expr => void; 380 }; 381 return false; 382 }; 383 384 @test fn parseident() void = { 385 let (ident, trailing) = parseident("hare::lex") as (ast::ident, bool); 386 defer ast::ident_free(ident); 387 assert(ast::ident_eq(ident, ["hare", "lex"])); 388 assert(!trailing); 389 390 let (ident, trailing) = parseident("rt::abort") as (ast::ident, bool); 391 defer ast::ident_free(ident); 392 assert(ast::ident_eq(ident, ["rt", "abort"])); 393 assert(!trailing); 394 395 let (ident, trailing) = parseident("foo::bar::") as (ast::ident, bool); 396 defer ast::ident_free(ident); 397 assert(ast::ident_eq(ident, ["foo", "bar"])); 398 assert(trailing); 399 assert(parseident("strings::dup*{}&@") is void); 400 assert(parseident("") is void); 401 assert(parseident("::") is void); 402 }; 403 404 fn get_init_funcs(matching: *[]ast::decl, notmatching: *[]ast::decl) void = { 405 if (len(matching) != 1) { 406 return; 407 }; 408 let ident = match (matching[0].decl) { 409 case let d: []ast::decl_type => 410 if (len(d) != 1 || len(d[0].ident) != 1) { 411 return; 412 }; 413 if (d[0]._type.flags & ast::type_flag::ERROR != 0) { 414 return; 415 }; 416 match (d[0]._type.repr) { 417 case let repr: ast::builtin_type => 418 if (repr == ast::builtin_type::VOID) { 419 return; 420 }; 421 case => void; 422 }; 423 yield d[0].ident; 424 case => 425 return; 426 }; 427 428 for (let i = 0z; i < len(notmatching); i += 1) { 429 let _type = match (notmatching[i].decl) { 430 case let d: []ast::decl_const => 431 yield match (d[0]._type) { 432 case let t: *ast::_type => 433 yield t; 434 case null => 435 continue; 436 }; 437 case let d: []ast::decl_global => 438 yield match (d[0]._type) { 439 case let t: *ast::_type => 440 yield t; 441 case null => 442 continue; 443 }; 444 case let d: ast::decl_func => 445 let _type = d.prototype.repr as ast::func_type; 446 yield _type.result; 447 case => 448 continue; 449 }; 450 451 if (is_init_type(ident, _type)) { 452 append(matching, notmatching[i]); 453 delete(notmatching[i]); 454 i -= 1; 455 }; 456 }; 457 }; 458 459 fn is_init_type(ident: ast::ident, _type: *ast::_type) bool = { 460 let type_ident = match (_type.repr) { 461 case let repr: ast::alias_type => 462 yield repr.ident; 463 case let repr: ast::list_type => 464 if (!(repr.length is ast::len_slice)) { 465 return false; 466 }; 467 yield match (repr.members.repr) { 468 case let repr: ast::alias_type => 469 yield repr.ident; 470 case => 471 return false; 472 }; 473 case let repr: ast::pointer_type => 474 yield match (repr.referent.repr) { 475 case let repr: ast::alias_type => 476 yield repr.ident; 477 case => 478 return false; 479 }; 480 case let repr: ast::tagged_type => 481 for (let t .. repr) { 482 if (is_init_type(ident, t)) { 483 return true; 484 }; 485 }; 486 return false; 487 case => 488 return false; 489 }; 490 491 return ast::ident_eq(ident, type_ident); 492 };