@@ -59,18 +59,72 @@ pub fn list_types(path: &Path) -> Result<Vec<TypeModel>> {
5959 if path. extension ( ) . is_some_and ( |ext| ext == "tsp" ) {
6060 let content = fs:: read_to_string ( & path) . with_context ( || format ! ( "Failed to read {}" , path. display( ) ) ) ?;
6161 let file_name = path. file_name ( ) . unwrap_or_default ( ) . to_string_lossy ( ) . to_string ( ) ;
62+ models. extend ( parse_models ( & content, & file_name) ) ;
63+ }
64+ }
6265
63- let doc_refs = extract_doc_refs ( & content) ;
66+ Ok ( models)
67+ }
6468
65- for line in content. lines ( ) {
66- if let Some ( name) = extract_model_name ( line) {
67- models. push ( TypeModel { name, file : file_name. clone ( ) , doc_refs : doc_refs. clone ( ) } ) ;
69+ /// Parse TypeSpec models, associating each `// @doc <id>` marker with the model it belongs to:
70+ /// - a marker inside a model body belongs to that model;
71+ /// - a marker directly preceding a `model` declaration (outside any block) belongs to that model.
72+ fn parse_models ( content : & str , file_name : & str ) -> Vec < TypeModel > {
73+ let mut models = Vec :: new ( ) ;
74+ let mut depth: usize = 0 ;
75+ let mut opened = false ;
76+ let mut current: Option < TypeModel > = None ;
77+ let mut pending_docs: Vec < String > = Vec :: new ( ) ;
78+ let mut in_model = false ;
79+
80+ for line in content. lines ( ) {
81+ let trimmed = line. trim ( ) ;
82+
83+ if let Some ( doc) = extract_doc_marker ( trimmed) {
84+ if in_model {
85+ if let Some ( m) = current. as_mut ( )
86+ && !m. doc_refs . iter ( ) . any ( |r| r == & doc)
87+ {
88+ m. doc_refs . push ( doc) ;
89+ }
90+ } else {
91+ pending_docs. push ( doc) ;
92+ }
93+ continue ;
94+ }
95+
96+ if let Some ( name) = extract_model_name ( trimmed) {
97+ let mut model = TypeModel { name, file : file_name. to_string ( ) , doc_refs : Vec :: new ( ) } ;
98+ if !pending_docs. is_empty ( ) {
99+ model. doc_refs = std:: mem:: take ( & mut pending_docs) ;
100+ }
101+ current = Some ( model) ;
102+ in_model = true ;
103+ }
104+
105+ if in_model {
106+ let opens = trimmed. bytes ( ) . filter ( |b| * b == b'{' ) . count ( ) ;
107+ let closes = trimmed. bytes ( ) . filter ( |b| * b == b'}' ) . count ( ) ;
108+ if opens > 0 {
109+ opened = true ;
110+ }
111+ depth += opens;
112+ depth = depth. saturating_sub ( closes) ;
113+ if opened && depth == 0 {
114+ if let Some ( m) = current. take ( ) {
115+ models. push ( m) ;
68116 }
117+ in_model = false ;
118+ opened = false ;
69119 }
70120 }
71121 }
72122
73- Ok ( models)
123+ if let Some ( m) = current. take ( ) {
124+ models. push ( m) ;
125+ }
126+
127+ models
74128}
75129
76130pub fn init_main_tsp ( path : & Path ) -> Result < ( ) > {
@@ -114,6 +168,17 @@ pub fn init_main_tsp(path: &Path) -> Result<()> {
114168 Ok ( ( ) )
115169}
116170
171+ fn extract_doc_marker ( line : & str ) -> Option < String > {
172+ let line = line. strip_prefix ( "//" ) ?. trim ( ) ;
173+ let after = line. strip_prefix ( "@doc " ) ?;
174+ let id: String = after. chars ( ) . take_while ( |c| !c. is_whitespace ( ) ) . collect ( ) ;
175+ if id. is_empty ( ) {
176+ None
177+ } else {
178+ Some ( id)
179+ }
180+ }
181+
117182fn collect_imports ( ts_dir : & Path ) -> Result < Vec < String > > {
118183 let mut imports = Vec :: new ( ) ;
119184
@@ -151,23 +216,6 @@ fn extract_model_name(line: &str) -> Option<String> {
151216 None
152217}
153218
154- fn extract_doc_refs ( content : & str ) -> Vec < String > {
155- let mut refs = Vec :: new ( ) ;
156- for line in content. lines ( ) {
157- let trimmed = line. trim ( ) ;
158- if let Some ( after_comment) = trimmed. strip_prefix ( "//" ) {
159- let after_comment = after_comment. trim ( ) ;
160- if let Some ( after_doc) = after_comment. strip_prefix ( "@doc " ) {
161- let ref_str: String = after_doc. chars ( ) . take_while ( |c| !c. is_whitespace ( ) ) . collect ( ) ;
162- if !ref_str. is_empty ( ) {
163- refs. push ( ref_str) ;
164- }
165- }
166- }
167- }
168- refs
169- }
170-
171219fn extract_import ( line : & str ) -> Option < String > {
172220 let trimmed = line. trim ( ) ;
173221 if let Some ( rest) = trimmed. strip_prefix ( "import \" " )
@@ -313,10 +361,41 @@ mod tests {
313361 }
314362
315363 #[ test]
316- fn test_extract_doc_refs ( ) {
317- let content = "// @doc TZ-100\n model X {}\n // @doc TZ-200\n model Y {}" ;
318- let refs = extract_doc_refs ( content) ;
319- assert_eq ! ( refs, vec![ "TZ-100" . to_string( ) , "TZ-200" . to_string( ) ] ) ;
364+ fn test_parse_models_marker_before_model ( ) {
365+ let models = parse_models ( "// @doc TZ-100\n model X {\n id: string;\n }\n // @doc TZ-200\n model Y {\n id: string;\n }" , "types.tsp" ) ;
366+ assert_eq ! ( models. len( ) , 2 ) ;
367+ assert_eq ! ( models[ 0 ] . name, "X" ) ;
368+ assert_eq ! ( models[ 0 ] . doc_refs, vec![ "TZ-100" . to_string( ) ] ) ;
369+ assert_eq ! ( models[ 1 ] . name, "Y" ) ;
370+ assert_eq ! ( models[ 1 ] . doc_refs, vec![ "TZ-200" . to_string( ) ] ) ;
371+ }
372+
373+ #[ test]
374+ fn test_parse_models_marker_inside_body ( ) {
375+ let models = parse_models ( "model A {\n // @doc TZ-001\n id: string;\n }\n model B {\n id: string;\n }" , "types.tsp" ) ;
376+ assert_eq ! ( models. len( ) , 2 ) ;
377+ assert_eq ! ( models[ 0 ] . name, "A" ) ;
378+ assert_eq ! ( models[ 0 ] . doc_refs, vec![ "TZ-001" . to_string( ) ] ) ;
379+ assert_eq ! ( models[ 1 ] . name, "B" ) ;
380+ assert ! ( models[ 1 ] . doc_refs. is_empty( ) ) ;
381+ }
382+
383+ #[ test]
384+ fn test_parse_models_single_line_and_no_markers ( ) {
385+ let models = parse_models ( "model P {\n id: string;\n }\n model Q { name: string; }\n " , "types.tsp" ) ;
386+ assert_eq ! ( models. len( ) , 2 ) ;
387+ assert ! ( models. iter( ) . all( |m| m. doc_refs. is_empty( ) ) ) ;
388+ }
389+
390+ #[ test]
391+ fn test_parse_models_distinct_refs_per_model ( ) {
392+ let models = parse_models (
393+ "// @doc tz-002-\n model Festival {\n id: string;\n }\n // @doc tz-004-\n model Quest {\n id: string;\n }" ,
394+ "models.tsp" ,
395+ ) ;
396+ assert_eq ! ( models. len( ) , 2 ) ;
397+ assert_eq ! ( models[ 0 ] . doc_refs, vec![ "tz-002-" . to_string( ) ] ) ;
398+ assert_eq ! ( models[ 1 ] . doc_refs, vec![ "tz-004-" . to_string( ) ] ) ;
320399 }
321400
322401 #[ test]
0 commit comments