@@ -233,3 +233,179 @@ def test_bare_call_with_operator_not_documented():
233233 decl_idx = next (i for i , l in enumerate (lines ) if "assert(cond)" in l )
234234 assert lines [decl_idx - 1 ].strip () == "*/"
235235 assert "Asserts a condition." in out
236+
237+
238+ def test_same_named_methods_mapped_by_source_order ():
239+ """When entries arrive out of source order (dependency order), each doc must land
240+ on the method at its own start_line, not get swapped (regression for the swap bug)."""
241+ aug = TSJSAugmentor ()
242+ src = (
243+ "class A {\n \t add(item, priority = 0) {\n \t \t return item;\n \t }\n }\n "
244+ "class B {\n \t add(key, value) {\n \t \t return key;\n \t }\n }\n "
245+ )
246+ # entries deliberately reversed (B.add first) to mimic dependency-order output
247+ docs = {
248+ "methods" : [
249+ ("Doc for B add." , {"method_name" : "add" , "start_line" : 7 }),
250+ ("Doc for A add." , {"method_name" : "add" , "start_line" : 2 }),
251+ ]
252+ }
253+ out = aug .augment ("f.ts" , src , docs )["f.ts" ]
254+ lines = out .splitlines ()
255+
256+ a_idx = next (i for i , l in enumerate (lines ) if "add(item, priority = 0)" in l )
257+ b_idx = next (i for i , l in enumerate (lines ) if "add(key, value)" in l )
258+ a_block = "\n " .join (lines [max (0 , a_idx - 4 ) : a_idx ])
259+ b_block = "\n " .join (lines [max (0 , b_idx - 4 ) : b_idx ])
260+
261+ assert "Doc for A add." in a_block
262+ assert "Doc for B add." in b_block
263+
264+
265+ def test_comment_body_not_matched_as_declaration ():
266+ """A JSDoc body line like ` * save(entity) ...` must not be matched as the method
267+ declaration (the generator `*` pattern must not hit comment lines and corrupt the
268+ file). Regenerating updates the real method's doc instead."""
269+ aug = TSJSAugmentor ()
270+ src = (
271+ "class Repo {\n "
272+ "\t /**\n "
273+ "\t * save(entity) persists the record.\n "
274+ "\t */\n "
275+ "\t save(entity) {\n "
276+ "\t \t return entity;\n "
277+ "\t }\n "
278+ "}\n "
279+ )
280+ out = aug .augment (
281+ "f.ts" ,
282+ src ,
283+ {"methods" : [("Persists the entity." , {"method_name" : "save" , "start_line" : 5 })]},
284+ )["f.ts" ]
285+
286+ assert "Persists the entity." in out
287+ # exactly one JSDoc block (the method's, replaced) -> no nested/corrupted /**
288+ assert out .count ("/**" ) == 1
289+ lines = out .splitlines ()
290+ decl = next (i for i , l in enumerate (lines ) if "save(entity) {" in l )
291+ assert lines [decl - 1 ].strip () == "*/"
292+
293+
294+ def test_block_comment_line_not_matched_as_declaration ():
295+ """A plain block-comment line starting with a method name must not be matched."""
296+ aug = TSJSAugmentor ()
297+ src = (
298+ "class Repo {\n "
299+ "\t /*\n "
300+ "\t load(id) is deprecated, use fetch instead.\n "
301+ "\t */\n "
302+ "\t load(id) {\n "
303+ "\t \t return id;\n "
304+ "\t }\n "
305+ "}\n "
306+ )
307+ out = aug .augment (
308+ "f.ts" ,
309+ src ,
310+ {"methods" : [("Loads by id." , {"method_name" : "load" , "start_line" : 5 })]},
311+ )["f.ts" ]
312+
313+ assert "Loads by id." in out
314+ # the doc must land above the real declaration, not inside the block comment
315+ lines = out .splitlines ()
316+ decl = next (i for i , l in enumerate (lines ) if "load(id) {" in l )
317+ assert lines [decl - 1 ].strip () == "*/"
318+ # the deprecated block-comment text is untouched (still present, not wrapped in /**)
319+ assert "load(id) is deprecated" in out
320+
321+
322+ def test_method_with_inline_block_comment_still_documented ():
323+ """A method whose declaration line has an inline `/* */` comment must still be
324+ documented (single-line block comments are not treated as comment lines)."""
325+ aug = TSJSAugmentor ()
326+ src = "class C {\n \t parse(url) /* TODO */ {\n \t \t return url;\n \t }\n }\n "
327+ out = aug .augment (
328+ "f.ts" ,
329+ src ,
330+ {"methods" : [("Parses a URL." , {"method_name" : "parse" , "start_line" : 2 })]},
331+ )["f.ts" ]
332+
333+ assert "Parses a URL." in out
334+ lines = out .splitlines ()
335+ decl = next (i for i , l in enumerate (lines ) if "parse(url)" in l )
336+ assert lines [decl - 1 ].strip () == "*/"
337+
338+
339+ def test_string_literal_slash_star_does_not_suppress_methods ():
340+ """A `/*` inside a string literal must NOT open a phantom comment block that
341+ suppresses documentation of later methods (literal-aware comment scan)."""
342+ aug = TSJSAugmentor ()
343+ src = "class Repo {\n " '\t glob = "src/*";\n ' "\t save(entity) {\n \t \t return entity;\n \t }\n " "}\n "
344+ out = aug .augment (
345+ "f.ts" ,
346+ src ,
347+ {"methods" : [("Saves the entity." , {"method_name" : "save" , "start_line" : 3 })]},
348+ )["f.ts" ]
349+
350+ assert "Saves the entity." in out
351+ lines = out .splitlines ()
352+ decl = next (i for i , l in enumerate (lines ) if "save(entity) {" in l )
353+ assert lines [decl - 1 ].strip () == "*/"
354+
355+
356+ def test_declaration_opening_trailing_block_comment_still_documented ():
357+ """A declaration line that also opens a multi-line comment after the code must still
358+ be documented (the code part is a real declaration)."""
359+ aug = TSJSAugmentor ()
360+ src = "class I {\n " "\t save(entity) { /* note:\n " "\t multi-line */\n " "\t \t return entity;\n " "\t }\n " "}\n "
361+ out = aug .augment (
362+ "f.ts" ,
363+ src ,
364+ {"methods" : [("Saves it." , {"method_name" : "save" , "start_line" : 2 })]},
365+ )["f.ts" ]
366+
367+ assert "Saves it." in out
368+ lines = out .splitlines ()
369+ decl = next (i for i , l in enumerate (lines ) if "save(entity)" in l and "*" != l .strip ()[:1 ])
370+ assert lines [decl - 1 ].strip () == "*/"
371+
372+
373+ def test_multiline_template_literal_does_not_suppress_methods ():
374+ """A multi-line template literal containing '/*' must not open a phantom comment
375+ block (quote state is threaded across lines), so later methods stay documented."""
376+ aug = TSJSAugmentor ()
377+ src = (
378+ "class Q {\n "
379+ "\t sql = `\n "
380+ "\t \t SELECT /* unclosed marker\n "
381+ "\t `;\n "
382+ "\t save(entity) {\n \t \t return entity;\n \t }\n "
383+ "}\n "
384+ )
385+ out = aug .augment (
386+ "f.ts" ,
387+ src ,
388+ {"methods" : [("Saves it." , {"method_name" : "save" , "start_line" : 5 })]},
389+ )["f.ts" ]
390+
391+ assert "Saves it." in out
392+ lines = out .splitlines ()
393+ decl = next (i for i , l in enumerate (lines ) if "save(entity) {" in l )
394+ assert lines [decl - 1 ].strip () == "*/"
395+
396+
397+ def test_regex_with_apostrophe_does_not_suppress_methods ():
398+ """A regex literal containing a lone quote char (e.g. /O'Brien/) must not leave a
399+ dangling string state that suppresses documentation of later methods."""
400+ aug = TSJSAugmentor ()
401+ src = "class M {\n " "\t re = /O'Brien/;\n " "\t save(entity) {\n \t \t return entity;\n \t }\n " "}\n "
402+ out = aug .augment (
403+ "f.ts" ,
404+ src ,
405+ {"methods" : [("Saves it." , {"method_name" : "save" , "start_line" : 3 })]},
406+ )["f.ts" ]
407+
408+ assert "Saves it." in out
409+ lines = out .splitlines ()
410+ decl = next (i for i , l in enumerate (lines ) if "save(entity) {" in l )
411+ assert lines [decl - 1 ].strip () == "*/"
0 commit comments