main.ha (8860B)
1 // License: GPL-3.0 2 // (c) 2021 Alexey Yerin <yyp@disroot.org> 3 // (c) 2021 Drew DeVault <sir@cmpwn.com> 4 // (c) 2021 Ember Sawady <ecs@d2evs.net> 5 // (c) 2022 Sebastian <sebastian@sebsite.pw> 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 strings; 20 use unix::tty; 21 22 type format = enum { 23 HARE, 24 TTY, 25 HTML, 26 }; 27 28 type context = struct { 29 mctx: *module::context, 30 ident: ast::ident, 31 tags: []module::tag, 32 version: module::version, 33 summary: summary, 34 format: format, 35 template: bool, 36 show_undocumented: bool, 37 readme: (io::file | void), 38 out: io::handle, 39 pager: (exec::process | void), 40 }; 41 42 export fn main() void = { 43 let fmt = if (tty::isatty(os::stdout_file)) { 44 yield format::TTY; 45 } else { 46 yield format::HARE; 47 }; 48 let template = true; 49 let show_undocumented = false; 50 let tags = match (default_tags()) { 51 case let t: []module::tag => 52 yield t; 53 case let err: exec::error => 54 fmt::fatal(strerror(err)); 55 }; 56 defer module::tags_free(tags); 57 58 const help: [_]getopt::help = [ 59 "reads and formats Hare documentation", 60 ('F', "format", "specify output format (hare, tty, or html)"), 61 ('T', "tags...", "set build tags"), 62 ('X', "tags...", "unset build tags"), 63 ('a', "show undocumented members (only applies to -Fhare and -Ftty)"), 64 ('t', "disable HTML template (requires postprocessing)"), 65 "[identifiers...]", 66 ]; 67 const cmd = getopt::parse(os::args, help...); 68 defer getopt::finish(&cmd); 69 70 for (let i = 0z; i < len(cmd.opts); i += 1) { 71 let opt = cmd.opts[i]; 72 switch (opt.0) { 73 case 'F' => 74 switch (opt.1) { 75 case "hare" => 76 fmt = format::HARE; 77 case "tty" => 78 fmt = format::TTY; 79 case "html" => 80 fmt = format::HTML; 81 case => 82 fmt::fatal("Invalid format", opt.1); 83 }; 84 case 'T' => 85 tags = match (addtags(tags, opt.1)) { 86 case void => 87 fmt::fatal("Error parsing tags"); 88 case let t: []module::tag => 89 yield t; 90 }; 91 case 'X' => 92 tags = match (deltags(tags, opt.1)) { 93 case void => 94 fmt::fatal("Error parsing tags"); 95 case let t: []module::tag => 96 yield t; 97 }; 98 case 't' => 99 template = false; 100 case 'a' => 101 show_undocumented = true; 102 case => abort(); 103 }; 104 }; 105 106 if (show_undocumented) switch (fmt) { 107 case format::HARE, format::TTY => void; 108 case => 109 fmt::fatal("Option -a must be used only with -Fhare or -Ftty"); 110 }; 111 112 let decls: []ast::decl = []; 113 defer free(decls); 114 115 let ctx = module::context_init(tags, [], default_harepath()); 116 defer module::context_finish(&ctx); 117 118 const id: ast::ident = 119 if (len(cmd.args) < 1) [] 120 else match (parseident(cmd.args[0])) { 121 case let err: parse::error => 122 fmt::fatal(parse::strerror(err)); 123 case let id: ast::ident => 124 yield id; 125 }; 126 127 let decl = ""; 128 let dirname: ast::ident = if (len(id) < 2) [] else id[..len(id) - 1]; 129 const version = match (module::lookup(&ctx, id)) { 130 case let ver: module::version => 131 yield ver; 132 case let err: module::error => 133 yield match (module::lookup(&ctx, dirname)) { 134 case let ver: module::version => 135 assert(len(id) >= 1); 136 decl = id[len(id) - 1]; 137 yield ver; 138 case let err: module::error => 139 fmt::fatal("Error scanning input module:", 140 module::strerror(err)); 141 }; 142 }; 143 144 for (let i = 0z; i < len(version.inputs); i += 1) { 145 const in = version.inputs[i]; 146 const ext = path::peek_ext(&path::init(in.path)!); 147 if (ext is void || ext as str != "ha") { 148 continue; 149 }; 150 match (scan(in.path)) { 151 case let u: ast::subunit => 152 ast::imports_finish(u.imports); 153 append(decls, u.decls...); 154 case let err: error => 155 fmt::fatal("Error:", strerror(err)); 156 }; 157 }; 158 159 const rpath = path::init(version.basedir, "README")!; 160 const readme: (io::file | void) = if (decl == "") { 161 yield match (os::open(path::string(&rpath))) { 162 case let err: fs::error => 163 yield void; 164 case let f: io::file => 165 yield f; 166 }; 167 } else void; 168 169 defer match (readme) { 170 case void => void; 171 case let f: io::file => 172 io::close(f)!; 173 }; 174 175 if (decl != "") { 176 let new: []ast::decl = []; 177 for (let i = 0z; i < len(decls); i += 1) { 178 if (has_decl(decls[i], decl)) { 179 append(new, decls[i]); 180 } else { 181 ast::decl_finish(decls[i]); 182 }; 183 }; 184 if (len(new) == 0) { 185 fmt::fatalf("Could not find {}::{}", 186 unparse::identstr(dirname), decl); 187 }; 188 free(decls); 189 decls = new; 190 191 show_undocumented = true; 192 }; 193 194 defer for (let i = 0z; i < len(decls); i += 1) { 195 ast::decl_finish(decls[i]); 196 }; 197 198 const ctx = context { 199 mctx = &ctx, 200 ident = id, 201 tags = tags, 202 version = version, 203 summary = sort_decls(decls), 204 format = fmt, 205 template = template, 206 readme = readme, 207 show_undocumented = show_undocumented, 208 out = os::stdout, 209 pager = void, 210 }; 211 212 if (fmt == format::TTY) { 213 ctx.out = init_tty(&ctx); 214 }; 215 216 match (emit(&ctx)) { 217 case void => void; 218 case let err: error => 219 fmt::fatal("Error:", strerror(err)); 220 }; 221 222 io::close(ctx.out)!; 223 match (ctx.pager) { 224 case void => void; 225 case let proc: exec::process => 226 exec::wait(&proc)!; 227 }; 228 }; 229 230 // Nearly identical to parse::identstr, except alphanumeric lexical tokens are 231 // converted to strings and there must be no trailing tokens that don't belong 232 // to the ident in the string. For example, this function will parse `rt::abort` 233 // as a valid identifier. 234 fn parseident(in: str) (ast::ident | parse::error) = { 235 const buf = memio::fixed(strings::toutf8(in)); 236 const lexer = lex::init(&buf, "<string>"); 237 defer lex::finish(&lexer); 238 let ident: []str = []; // TODO: errdefer 239 let z = 0z; 240 for (true) { 241 const tok = lex::lex(&lexer)?; 242 const name = if (tok.0 == lex::ltok::NAME) { 243 yield tok.1 as str; 244 } else if (tok.0 < lex::ltok::LAST_KEYWORD) { 245 yield lex::tokstr(tok); 246 } else { 247 lex::unlex(&lexer, tok); 248 const loc = lex::mkloc(&lexer); 249 const why = "Unexpected trailing :: in ident"; 250 return (loc, why): lex::syntax: parse::error; 251 }; 252 append(ident, name); 253 z += len(name); 254 const tok = lex::lex(&lexer)?; 255 switch (tok.0) { 256 case lex::ltok::EOF => 257 break; 258 case lex::ltok::DOUBLE_COLON => 259 z += 1; 260 case => 261 lex::unlex(&lexer, tok); 262 const loc = lex::mkloc(&lexer); 263 const why = fmt::asprintf("Unexpected '{}' in ident", 264 lex::tokstr(tok)); 265 return (loc, why): lex::syntax: parse::error; 266 }; 267 }; 268 if (z > ast::IDENT_MAX) { 269 const loc = lex::mkloc(&lexer); 270 const why = "Identifier exceeds maximum length"; 271 return (loc, why): lex::syntax: parse::error; 272 }; 273 return ident; 274 }; 275 276 fn init_tty(ctx: *context) io::handle = { 277 const pager = match (os::getenv("PAGER")) { 278 case let name: str => 279 yield match (exec::cmd(name)) { 280 case let cmd: exec::command => 281 yield cmd; 282 case exec::error => 283 return os::stdout; 284 }; 285 case void => 286 yield match (exec::cmd("less", "-R")) { 287 case let cmd: exec::command => 288 yield cmd; 289 case exec::error => 290 yield match (exec::cmd("more", "-R")) { 291 case let cmd: exec::command => 292 yield cmd; 293 case exec::error => 294 return os::stdout; 295 }; 296 }; 297 }; 298 299 const pipe = exec::pipe(); 300 exec::addfile(&pager, os::stdin_file, pipe.0); 301 // Get raw flag in if possible 302 exec::setenv(&pager, "LESS", os::tryenv("LESS", "FRX"))!; 303 exec::setenv(&pager, "MORE", os::tryenv("MORE", "R"))!; 304 ctx.pager = exec::start(&pager)!; 305 return pipe.1; 306 }; 307 308 fn has_decl(decl: ast::decl, name: str) bool = { 309 if (!decl.exported) { 310 return false; 311 }; 312 313 match (decl.decl) { 314 case let d: []ast::decl_const => 315 for (let i = 0z; i < len(d); i += 1) { 316 if (len(d[i].ident) == 1 && d[i].ident[0] == name) { 317 return true; 318 }; 319 }; 320 case let d: ast::decl_func => 321 if (len(d.ident) == 1 && d.ident[0] == name) { 322 return true; 323 }; 324 const sym = strings::split(d.symbol, "."); 325 defer free(sym); 326 return len(sym) > 0 && sym[len(sym) - 1] == name; 327 case let d: []ast::decl_global => 328 for (let i = 0z; i < len(d); i += 1) { 329 if (len(d[i].ident) == 1 && d[i].ident[0] == name) { 330 return true; 331 }; 332 }; 333 case let d: []ast::decl_type => 334 for (let i = 0z; i < len(d); i += 1) { 335 if (len(d[i].ident) == 1 && d[i].ident[0] == name) { 336 return true; 337 }; 338 }; 339 }; 340 return false; 341 }; 342 343 fn scan(path: str) (ast::subunit | error) = { 344 const input = match (os::open(path)) { 345 case let f: io::file => 346 yield f; 347 case let err: fs::error => 348 fmt::fatalf("Error reading {}: {}", path, fs::strerror(err)); 349 }; 350 defer io::close(input)!; 351 const lexer = lex::init(input, path, lex::flag::COMMENTS); 352 return parse::subunit(&lexer)?; 353 }; 354 355 fn emit(ctx: *context) (void | error) = { 356 switch (ctx.format) { 357 case format::HARE => 358 emit_hare(ctx)?; 359 case format::TTY => 360 emit_tty(ctx)?; 361 case format::HTML => 362 emit_html(ctx)?; 363 }; 364 }; 365 366 @test fn parseident() void = { 367 assert(parseident("hare::lex") is ast::ident); 368 assert(parseident("strings::dup*{}&@") is parse::error); 369 assert(parseident("foo::bar::") is parse::error); 370 assert(parseident("rt::abort") is ast::ident); 371 };