Coverage for tests / unit / utils / test_jsonc.py: 100%

65 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Unit tests for lintro.utils.jsonc module.""" 

2 

3from __future__ import annotations 

4 

5import json 

6 

7import pytest 

8from assertpy import assert_that 

9 

10from lintro.utils.jsonc import load_jsonc, strip_jsonc_comments, strip_trailing_commas 

11 

12# ============================================================================= 

13# Tests for strip_jsonc_comments 

14# ============================================================================= 

15 

16 

17def test_strip_jsonc_comments_no_comments() -> None: 

18 """Return unchanged content when no comments are present.""" 

19 content = '{"key": "value", "num": 42}' 

20 result = strip_jsonc_comments(content) 

21 assert_that(result).is_equal_to(content) 

22 

23 

24def test_strip_jsonc_comments_line_comment() -> None: 

25 """Strip single-line // comments.""" 

26 content = '{"key": "value"} // this is a comment' 

27 result = strip_jsonc_comments(content) 

28 assert_that(result.strip()).is_equal_to('{"key": "value"}') 

29 

30 

31def test_strip_jsonc_comments_block_comment() -> None: 

32 """Strip /* */ block comments.""" 

33 content = '{"key": /* comment */ "value"}' 

34 result = strip_jsonc_comments(content) 

35 parsed = json.loads(result) 

36 assert_that(parsed["key"]).is_equal_to("value") 

37 

38 

39def test_strip_jsonc_comments_preserves_strings() -> None: 

40 """Preserve // and /* patterns inside string values.""" 

41 content = '{"url": "https://example.com"}' 

42 result = strip_jsonc_comments(content) 

43 parsed = json.loads(result) 

44 assert_that(parsed["url"]).is_equal_to("https://example.com") 

45 

46 

47# ============================================================================= 

48# Tests for strip_trailing_commas 

49# ============================================================================= 

50 

51 

52def test_strip_trailing_commas_removes_trailing_comma_before_brace() -> None: 

53 """Remove trailing comma before closing brace.""" 

54 content = '{"a": 1, "b": 2,}' 

55 result = strip_trailing_commas(content) 

56 assert_that(result).is_equal_to('{"a": 1, "b": 2}') 

57 

58 

59def test_strip_trailing_commas_removes_trailing_comma_before_bracket() -> None: 

60 """Remove trailing comma before closing bracket.""" 

61 content = '["a", "b",]' 

62 result = strip_trailing_commas(content) 

63 assert_that(result).is_equal_to('["a", "b"]') 

64 

65 

66def test_strip_trailing_commas_handles_whitespace() -> None: 

67 """Remove trailing comma with whitespace before closing.""" 

68 content = '{"a": 1,\n}' 

69 result = strip_trailing_commas(content) 

70 assert_that(result).is_equal_to('{"a": 1\n}') 

71 

72 

73def test_strip_trailing_commas_no_trailing_comma() -> None: 

74 """Return unchanged content when no trailing commas.""" 

75 content = '{"a": 1, "b": 2}' 

76 result = strip_trailing_commas(content) 

77 assert_that(result).is_equal_to(content) 

78 

79 

80# ============================================================================= 

81# Tests for load_jsonc 

82# ============================================================================= 

83 

84 

85def test_load_jsonc_plain_json() -> None: 

86 """Parse plain JSON without comments or trailing commas.""" 

87 result = load_jsonc('{"key": "value"}') 

88 assert_that(result).is_equal_to({"key": "value"}) 

89 

90 

91def test_load_jsonc_with_comments() -> None: 

92 """Parse JSONC with line and block comments.""" 

93 content = """{ 

94 // A line comment 

95 "name": "test", 

96 /* block comment */ 

97 "value": 42 

98}""" 

99 result = load_jsonc(content) 

100 assert_that(result["name"]).is_equal_to("test") 

101 assert_that(result["value"]).is_equal_to(42) 

102 

103 

104def test_load_jsonc_with_trailing_commas() -> None: 

105 """Parse JSONC with trailing commas.""" 

106 content = '{"a": 1, "b": [1, 2, 3,],}' 

107 result = load_jsonc(content) 

108 assert_that(result["a"]).is_equal_to(1) 

109 assert_that(result["b"]).is_equal_to([1, 2, 3]) 

110 

111 

112def test_load_jsonc_with_comments_and_trailing_commas() -> None: 

113 """Parse JSONC with both comments and trailing commas.""" 

114 content = """{ 

115 // compiler settings 

116 "compilerOptions": { 

117 "strict": true, // enable strict mode 

118 "typeRoots": [ 

119 "./custom-types", 

120 "./node_modules/@types", 

121 ], 

122 }, 

123}""" 

124 result = load_jsonc(content) 

125 assert_that(result["compilerOptions"]["strict"]).is_true() 

126 assert_that(result["compilerOptions"]["typeRoots"]).is_equal_to( 

127 ["./custom-types", "./node_modules/@types"], 

128 ) 

129 

130 

131def test_load_jsonc_invalid_json_raises() -> None: 

132 """Raise JSONDecodeError for invalid JSON after stripping.""" 

133 with pytest.raises(json.JSONDecodeError): 

134 load_jsonc("{invalid}") 

135 

136 

137def test_load_jsonc_tsconfig_with_comments_and_type_roots() -> None: 

138 """Parse a realistic tsconfig.json with JSONC features and typeRoots. 

139 

140 This is the primary scenario from issue #570. 

141 """ 

142 content = """{ 

143 // TypeScript configuration 

144 "compilerOptions": { 

145 "target": "ES2020", 

146 "module": "ESNext", 

147 "strict": true, 

148 /* Custom type roots for monorepo */ 

149 "typeRoots": [ 

150 "./types", 

151 "./node_modules/@types", 

152 ], 

153 "outDir": "./dist", 

154 }, 

155 "include": ["src/**/*.ts"], 

156 "exclude": ["node_modules"], 

157}""" 

158 result = load_jsonc(content) 

159 assert_that(result["compilerOptions"]["typeRoots"]).is_equal_to( 

160 ["./types", "./node_modules/@types"], 

161 ) 

162 assert_that(result["compilerOptions"]["strict"]).is_true()