main.odin (5480B)
1 // odin-review-extract prints the declarations of the Odin files it is given, 2 // as JSON, using Odin's own parser (core:odin). One line per invocation: 3 // 4 // odin-review-extract file.odin ... > decls.json 5 // 6 // where the JSON is {"files":[{"name":"a.odin","decls":[ 7 // {"name":"main","kind":"func","line":7,"end_line":9, 8 // "exported":true,"test":false,"text":"...","doc":"..."}]}]} 9 // 10 // Odin has no export keyword: a package-level declaration is the package's 11 // API, so it is reported exported unless marked @(private=...). 12 package main 13 14 import "core:encoding/json" 15 import "core:fmt" 16 import "core:odin/ast" 17 import "core:odin/parser" 18 import "core:os" 19 import "core:strings" 20 21 Decl :: struct { 22 name: string, 23 kind: string, 24 line: int, 25 end_line: int, 26 exported: bool, 27 test: bool, 28 text: string, 29 doc: string, 30 } 31 32 File_Result :: struct { 33 name: string, 34 decls: [dynamic]Decl, 35 } 36 37 Output :: struct { 38 files: [dynamic]File_Result, 39 } 40 41 main :: proc() { 42 args := os.args[1:] 43 out := Output{} 44 for arg in args { 45 data, err := os.read_entire_file_from_path(arg, context.allocator) 46 if err != nil { 47 continue 48 } 49 append(&out.files, read_file(arg, data)) 50 } 51 data, err := json.marshal(out, json.Marshal_Options{pretty = true, use_spaces = true}) 52 if err != nil { 53 fmt.eprintf("odin-review-extract: %v\n", err) 54 os.exit(1) 55 } 56 fmt.print(string(data)) 57 } 58 59 read_file :: proc(path: string, src: []byte) -> File_Result { 60 result := File_Result { 61 name = path, 62 } 63 file := new(ast.File) 64 file.fullpath = path 65 file.src = string(src) 66 67 p := parser.default_parser() 68 parser.parse_file(&p, file) 69 70 for stmt in file.decls { 71 decl, ok := stmt.derived_stmt.(^ast.Value_Decl) 72 if !ok { 73 continue // Imports and foreign blocks are not this file's work. 74 } 75 private := is_private(decl.attributes[:]) 76 for name_expr in decl.names { 77 ident, is_ident := name_expr.derived_expr.(^ast.Ident) 78 if !is_ident { 79 continue 80 } 81 entry := Decl { 82 name = ident.name, 83 kind = kind_of(decl), 84 line = int(name_expr.pos.line), 85 end_line = int(stmt.end.line), 86 exported = private == false, 87 text = line_text(src, name_expr.pos.line), 88 doc = doc_text(decl.docs), 89 } 90 if kind_of(decl) == "func" { 91 entry.test = is_test(decl.attributes[:]) 92 } 93 append(&result.decls, entry) 94 } 95 } 96 return result 97 } 98 99 // kind_of reports what a value declaration declares: a procedure, a type, or 100 // a value. The type sits on the declaration, or in its only value when the 101 // declaration is `name :: thing`. 102 kind_of :: proc(decl: ^ast.Value_Decl) -> string { 103 kind := "value" 104 if decl.type != nil { 105 kind = expr_kind(decl.type.derived_expr) 106 } 107 for value in decl.values { 108 if k := expr_kind(value.derived_expr); k != "value" { 109 return k 110 } 111 } 112 return kind 113 } 114 115 expr_kind :: proc(e: ast.Any_Expr) -> string { 116 #partial switch v in e { 117 case ^ast.Proc_Lit: 118 return "func" 119 case ^ast.Proc_Group: 120 return "func" 121 case ^ast.Struct_Type, 122 ^ast.Union_Type, 123 ^ast.Enum_Type, 124 ^ast.Bit_Set_Type, 125 ^ast.Distinct_Type, 126 ^ast.Poly_Type, 127 ^ast.Typeid_Type, 128 ^ast.Pointer_Type, 129 ^ast.Array_Type, 130 ^ast.Dynamic_Array_Type, 131 ^ast.Fixed_Capacity_Dynamic_Array_Type, 132 ^ast.Map_Type, 133 ^ast.Relative_Type, 134 ^ast.Matrix_Type, 135 ^ast.Bit_Field_Type: 136 return "type" 137 case: 138 return "value" 139 } 140 return "value" 141 } 142 143 // is_test reports the @(test) attribute, which the testing package runs. 144 is_test :: proc(attributes: []^ast.Attribute) -> bool { 145 for attribute in attributes { 146 for elem in attribute.elems { 147 #partial switch v in elem.derived_expr { 148 case ^ast.Ident: 149 if v.name == "test" { 150 return true 151 } 152 case ^ast.Field_Value: 153 if field, ok := v.field.derived_expr.(^ast.Ident); ok && field.name == "test" { 154 return true 155 } 156 } 157 } 158 } 159 return false 160 } 161 162 // is_private reports an @(private=...) attribute, which keeps a declaration 163 // out of the package's public surface. 164 is_private :: proc(attributes: []^ast.Attribute) -> bool { 165 for attribute in attributes { 166 for elem in attribute.elems { 167 if field, is_field := elem.derived_expr.(^ast.Field_Value); is_field { 168 if name, named := field.field.derived_expr.(^ast.Ident); 169 named && name.name == "private" { 170 return true 171 } 172 } 173 } 174 } 175 return false 176 } 177 178 doc_text :: proc(group: ^ast.Comment_Group) -> string { 179 if group == nil { 180 return "" 181 } 182 parts: [dynamic]string 183 for token in group.list { 184 t := strings.trim_space(token.text) 185 switch { 186 case strings.has_prefix(t, "///"): 187 t = strings.trim_prefix(t, "///") 188 case strings.has_prefix(t, "//"): 189 t = strings.trim_prefix(t, "//") 190 case strings.has_prefix(t, "/*"): 191 t = strings.trim_suffix(strings.trim_prefix(t, "/*"), "*/") 192 for raw in strings.split(t, "\n") { 193 line := strings.trim_space(raw) 194 line = strings.trim_prefix(line, "*") 195 line = strings.trim_space(line) 196 if len(line) > 0 { 197 append(&parts, line) 198 } 199 } 200 continue 201 } 202 t = strings.trim_space(t) 203 if len(t) > 0 { 204 append(&parts, t) 205 } 206 } 207 defer delete(parts) 208 return strings.join(parts[:], " ") 209 } 210 211 line_text :: proc(src: []byte, line: int) -> string { 212 current := 1 213 start := 0 214 for i := 0; i < len(src); i += 1 { 215 if src[i] == '\n' { 216 if current == line { 217 return strings.trim_space(string(src[start:i])) 218 } 219 current += 1 220 start = i + 1 221 } 222 } 223 if current == line { 224 return strings.trim_space(string(src[start:])) 225 } 226 return "" 227 }