review

review patchsets using your default editor
Log | Files | Refs

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 }