1# Copyright 2023 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Tests for attribute_checker.py.""" 16 17import unittest 18from compiler.back_end.cpp import header_generator 19from compiler.front_end import glue 20from compiler.util import error 21from compiler.util import ir_data 22from compiler.util import ir_data_utils 23from compiler.util import test_util 24 25def _make_ir_from_emb(emb_text, name="m.emb"): 26 ir, unused_debug_info, errors = glue.parse_emboss_file( 27 name, 28 test_util.dict_file_reader({name: emb_text})) 29 assert not errors 30 return ir 31 32 33class NormalizeIrTest(unittest.TestCase): 34 35 def test_accepts_string_attribute(self): 36 ir = _make_ir_from_emb('[(cpp) namespace: "foo"]\n') 37 self.assertEqual([], header_generator.generate_header(ir)[1]) 38 39 def test_rejects_wrong_type_for_string_attribute(self): 40 ir = _make_ir_from_emb("[(cpp) namespace: 9]\n") 41 attr = ir.module[0].attribute[0] 42 self.assertEqual([[ 43 error.error("m.emb", attr.value.source_location, 44 "Attribute '(cpp) namespace' must have a string value.") 45 ]], header_generator.generate_header(ir)[1]) 46 47 def test_rejects_emboss_internal_attribute_with_back_end_specifier(self): 48 ir = _make_ir_from_emb('[(cpp) byte_order: "LittleEndian"]\n') 49 attr = ir.module[0].attribute[0] 50 self.assertEqual([[ 51 error.error("m.emb", attr.name.source_location, 52 "Unknown attribute '(cpp) byte_order' on module 'm.emb'.") 53 ]], header_generator.generate_header(ir)[1]) 54 55 def test_accepts_enum_case(self): 56 mod_ir = _make_ir_from_emb('[(cpp) $default enum_case: "kCamelCase"]') 57 self.assertEqual([], header_generator.generate_header(mod_ir)[1]) 58 enum_ir = _make_ir_from_emb('enum Foo:\n' 59 ' [(cpp) $default enum_case: "kCamelCase"]\n' 60 ' BAR = 1\n' 61 ' BAZ = 2\n') 62 self.assertEqual([], header_generator.generate_header(enum_ir)[1]) 63 enum_value_ir = _make_ir_from_emb('enum Foo:\n' 64 ' BAR = 1 [(cpp) enum_case: "kCamelCase"]\n' 65 ' BAZ = 2\n' 66 ' [(cpp) enum_case: "kCamelCase"]\n') 67 self.assertEqual([], header_generator.generate_header(enum_value_ir)[1]) 68 enum_in_struct_ir = _make_ir_from_emb('struct Outer:\n' 69 ' [(cpp) $default enum_case: "kCamelCase"]\n' 70 ' enum Inner:\n' 71 ' BAR = 1\n' 72 ' BAZ = 2\n') 73 self.assertEqual([], header_generator.generate_header(enum_in_struct_ir)[1]) 74 enum_in_bits_ir = _make_ir_from_emb('bits Outer:\n' 75 ' [(cpp) $default enum_case: "kCamelCase"]\n' 76 ' enum Inner:\n' 77 ' BAR = 1\n' 78 ' BAZ = 2\n') 79 self.assertEqual([], header_generator.generate_header(enum_in_bits_ir)[1]) 80 enum_ir = _make_ir_from_emb('enum Foo:\n' 81 ' [(cpp) $default enum_case: "SHOUTY_CASE,"]\n' 82 ' BAR = 1\n' 83 ' BAZ = 2\n') 84 self.assertEqual([], header_generator.generate_header(enum_ir)[1]) 85 enum_ir = _make_ir_from_emb('enum Foo:\n' 86 ' [(cpp) $default enum_case: "SHOUTY_CASE ,kCamelCase"]\n' 87 ' BAR = 1\n' 88 ' BAZ = 2\n') 89 self.assertEqual([], header_generator.generate_header(enum_ir)[1]) 90 91 def test_rejects_bad_enum_case_at_start(self): 92 ir = _make_ir_from_emb('enum Foo:\n' 93 ' [(cpp) $default enum_case: "SHORTY_CASE, kCamelCase"]\n' 94 ' BAR = 1\n' 95 ' BAZ = 2\n') 96 attr = ir.module[0].type[0].attribute[0] 97 98 bad_case_source_location = ir_data.Location() 99 bad_case_source_location = ir_data_utils.builder(bad_case_source_location) 100 bad_case_source_location.CopyFrom(attr.value.source_location) 101 # Location of SHORTY_CASE in the attribute line. 102 bad_case_source_location.start.column = 30 103 bad_case_source_location.end.column = 41 104 105 self.assertEqual([[ 106 error.error("m.emb", bad_case_source_location, 107 'Unsupported enum case "SHORTY_CASE", ' 108 'supported cases are: SHOUTY_CASE, kCamelCase.') 109 ]], header_generator.generate_header(ir)[1]) 110 111 def test_rejects_bad_enum_case_in_middle(self): 112 ir = _make_ir_from_emb('enum Foo:\n' 113 ' [(cpp) $default enum_case: "SHOUTY_CASE, bad_CASE, kCamelCase"]\n' 114 ' BAR = 1\n' 115 ' BAZ = 2\n') 116 attr = ir.module[0].type[0].attribute[0] 117 118 bad_case_source_location = ir_data.Location() 119 bad_case_source_location = ir_data_utils.builder(bad_case_source_location) 120 bad_case_source_location.CopyFrom(attr.value.source_location) 121 # Location of bad_CASE in the attribute line. 122 bad_case_source_location.start.column = 43 123 bad_case_source_location.end.column = 51 124 125 self.assertEqual([[ 126 error.error("m.emb", bad_case_source_location, 127 'Unsupported enum case "bad_CASE", ' 128 'supported cases are: SHOUTY_CASE, kCamelCase.') 129 ]], header_generator.generate_header(ir)[1]) 130 131 def test_rejects_bad_enum_case_at_end(self): 132 ir = _make_ir_from_emb('enum Foo:\n' 133 ' [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase, BAD_case"]\n' 134 ' BAR = 1\n' 135 ' BAZ = 2\n') 136 attr = ir.module[0].type[0].attribute[0] 137 138 bad_case_source_location = ir_data.Location() 139 bad_case_source_location = ir_data_utils.builder(bad_case_source_location) 140 bad_case_source_location.CopyFrom(attr.value.source_location) 141 # Location of BAD_case in the attribute line. 142 bad_case_source_location.start.column = 55 143 bad_case_source_location.end.column = 63 144 145 self.assertEqual([[ 146 error.error("m.emb", bad_case_source_location, 147 'Unsupported enum case "BAD_case", ' 148 'supported cases are: SHOUTY_CASE, kCamelCase.') 149 ]], header_generator.generate_header(ir)[1]) 150 151 def test_rejects_duplicate_enum_case(self): 152 ir = _make_ir_from_emb('enum Foo:\n' 153 ' [(cpp) $default enum_case: "SHOUTY_CASE, SHOUTY_CASE"]\n' 154 ' BAR = 1\n' 155 ' BAZ = 2\n') 156 attr = ir.module[0].type[0].attribute[0] 157 158 bad_case_source_location = ir_data.Location() 159 bad_case_source_location = ir_data_utils.builder(bad_case_source_location) 160 bad_case_source_location.CopyFrom(attr.value.source_location) 161 # Location of the second SHOUTY_CASE in the attribute line. 162 bad_case_source_location.start.column = 43 163 bad_case_source_location.end.column = 54 164 165 self.assertEqual([[ 166 error.error("m.emb", bad_case_source_location, 167 'Duplicate enum case "SHOUTY_CASE".') 168 ]], header_generator.generate_header(ir)[1]) 169 170 171 def test_rejects_empty_enum_case(self): 172 # Double comma 173 ir = _make_ir_from_emb('enum Foo:\n' 174 ' [(cpp) $default enum_case: "SHOUTY_CASE,, kCamelCase"]\n' 175 ' BAR = 1\n' 176 ' BAZ = 2\n') 177 attr = ir.module[0].type[0].attribute[0] 178 179 bad_case_source_location = ir_data.Location() 180 bad_case_source_location = ir_data_utils.builder(bad_case_source_location) 181 bad_case_source_location.CopyFrom(attr.value.source_location) 182 # Location of excess comma. 183 bad_case_source_location.start.column = 42 184 bad_case_source_location.end.column = 42 185 186 self.assertEqual([[ 187 error.error("m.emb", bad_case_source_location, 188 'Empty enum case (or excess comma).') 189 ]], header_generator.generate_header(ir)[1]) 190 191 # Leading comma 192 ir = _make_ir_from_emb('enum Foo:\n' 193 ' [(cpp) $default enum_case: ", SHOUTY_CASE, kCamelCase"]\n' 194 ' BAR = 1\n' 195 ' BAZ = 2\n') 196 197 bad_case_source_location.start.column = 30 198 bad_case_source_location.end.column = 30 199 200 self.assertEqual([[ 201 error.error("m.emb", bad_case_source_location, 202 'Empty enum case (or excess comma).') 203 ]], header_generator.generate_header(ir)[1]) 204 205 # Excess trailing comma 206 ir = _make_ir_from_emb('enum Foo:\n' 207 ' [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase,,"]\n' 208 ' BAR = 1\n' 209 ' BAZ = 2\n') 210 211 bad_case_source_location.start.column = 54 212 bad_case_source_location.end.column = 54 213 214 self.assertEqual([[ 215 error.error("m.emb", bad_case_source_location, 216 'Empty enum case (or excess comma).') 217 ]], header_generator.generate_header(ir)[1]) 218 219 # Whitespace enum case 220 ir = _make_ir_from_emb('enum Foo:\n' 221 ' [(cpp) $default enum_case: "SHOUTY_CASE, , kCamelCase"]\n' 222 ' BAR = 1\n' 223 ' BAZ = 2\n') 224 225 bad_case_source_location.start.column = 45 226 bad_case_source_location.end.column = 45 227 228 self.assertEqual([[ 229 error.error("m.emb", bad_case_source_location, 230 'Empty enum case (or excess comma).') 231 ]], header_generator.generate_header(ir)[1]) 232 233 # Empty enum_case string 234 ir = _make_ir_from_emb('enum Foo:\n' 235 ' [(cpp) $default enum_case: ""]\n' 236 ' BAR = 1\n' 237 ' BAZ = 2\n') 238 239 bad_case_source_location.start.column = 30 240 bad_case_source_location.end.column = 30 241 242 self.assertEqual([[ 243 error.error("m.emb", bad_case_source_location, 244 'Empty enum case (or excess comma).') 245 ]], header_generator.generate_header(ir)[1]) 246 247 # Whitespace enum_case string 248 ir = _make_ir_from_emb('enum Foo:\n' 249 ' [(cpp) $default enum_case: " "]\n' 250 ' BAR = 1\n' 251 ' BAZ = 2\n') 252 253 bad_case_source_location.start.column = 35 254 bad_case_source_location.end.column = 35 255 256 self.assertEqual([[ 257 error.error("m.emb", bad_case_source_location, 258 'Empty enum case (or excess comma).') 259 ]], header_generator.generate_header(ir)[1]) 260 261 # One-character whitespace enum_case string 262 ir = _make_ir_from_emb('enum Foo:\n' 263 ' [(cpp) $default enum_case: " "]\n' 264 ' BAR = 1\n' 265 ' BAZ = 2\n') 266 267 bad_case_source_location.start.column = 31 268 bad_case_source_location.end.column = 31 269 270 self.assertEqual([[ 271 error.error("m.emb", bad_case_source_location, 272 'Empty enum case (or excess comma).') 273 ]], header_generator.generate_header(ir)[1]) 274 275 def test_accepts_namespace(self): 276 for test in [ 277 '[(cpp) namespace: "basic"]\n', 278 '[(cpp) namespace: "multiple::components"]\n', 279 '[(cpp) namespace: "::absolute"]\n', 280 '[(cpp) namespace: "::fully::qualified"]\n', 281 '[(cpp) namespace: "CAN::Be::cAPITAL"]\n', 282 '[(cpp) namespace: "trailingNumbers54321"]\n', 283 '[(cpp) namespace: "containing4321numbers"]\n', 284 '[(cpp) namespace: "can_have_underscores"]\n', 285 '[(cpp) namespace: "_initial_underscore"]\n', 286 '[(cpp) namespace: "_initial::_underscore"]\n', 287 '[(cpp) namespace: "::_initial::_underscore"]\n', 288 '[(cpp) namespace: "trailing_underscore_"]\n', 289 '[(cpp) namespace: "trailing_::underscore_"]\n', 290 '[(cpp) namespace: "::trailing_::underscore_"]\n', 291 '[(cpp) namespace: " spaces "]\n', 292 '[(cpp) namespace: "with :: spaces"]\n', 293 '[(cpp) namespace: " ::fully:: qualified :: with::spaces"]\n', 294 ]: 295 ir = _make_ir_from_emb(test) 296 self.assertEqual([], header_generator.generate_header(ir)[1]) 297 298 def test_rejects_non_namespace_strings(self): 299 for test in [ 300 '[(cpp) namespace: "5th::avenue"]\n', 301 '[(cpp) namespace: "can\'t::have::apostrophe"]\n', 302 '[(cpp) namespace: "cannot-have-dash"]\n', 303 '[(cpp) namespace: "no/slashes"]\n', 304 '[(cpp) namespace: "no\\\\slashes"]\n', 305 '[(cpp) namespace: "apostrophes*are*rejected"]\n', 306 '[(cpp) namespace: "avoid.dot"]\n', 307 '[(cpp) namespace: "does5+5"]\n', 308 '[(cpp) namespace: "=10"]\n', 309 '[(cpp) namespace: "?"]\n', 310 '[(cpp) namespace: "reject::spaces in::components"]\n', 311 '[(cpp) namespace: "totally::valid::but::extra +"]\n', 312 '[(cpp) namespace: "totally::valid::but::extra ::?"]\n', 313 '[(cpp) namespace: "< totally::valid::but::extra"]\n', 314 '[(cpp) namespace: "< ::totally::valid::but::extra"]\n', 315 '[(cpp) namespace: "::totally::valid::but::extra::"]\n', 316 '[(cpp) namespace: ":::extra::colon"]\n', 317 '[(cpp) namespace: "::extra:::colon"]\n', 318 ]: 319 ir = _make_ir_from_emb(test) 320 attr = ir.module[0].attribute[0] 321 self.assertEqual([[ 322 error.error("m.emb", attr.value.source_location, 323 'Invalid namespace, must be a valid C++ namespace, such ' 324 'as "abc", "abc::def", or "::abc::def::ghi" (ISO/IEC ' 325 '14882:2017 enclosing-namespace-specifier).') 326 ]], header_generator.generate_header(ir)[1]) 327 328 def test_rejects_empty_namespace(self): 329 for test in [ 330 '[(cpp) namespace: ""]\n', 331 '[(cpp) namespace: " "]\n', 332 '[(cpp) namespace: " "]\n', 333 ]: 334 ir = _make_ir_from_emb(test) 335 attr = ir.module[0].attribute[0] 336 self.assertEqual([[ 337 error.error("m.emb", attr.value.source_location, 338 'Empty namespace value is not allowed.') 339 ]], header_generator.generate_header(ir)[1]) 340 341 def test_rejects_global_namespace(self): 342 for test in [ 343 '[(cpp) namespace: "::"]\n', 344 '[(cpp) namespace: " ::"]\n', 345 '[(cpp) namespace: ":: "]\n', 346 '[(cpp) namespace: " :: "]\n', 347 ]: 348 ir = _make_ir_from_emb(test) 349 attr = ir.module[0].attribute[0] 350 self.assertEqual([[ 351 error.error("m.emb", attr.value.source_location, 352 'Global namespace is not allowed.') 353 ]], header_generator.generate_header(ir)[1]) 354 355 def test_rejects_reserved_namespace(self): 356 for test, expected in [ 357 # Only component 358 ('[(cpp) namespace: "class"]\n', 'class'), 359 # Only component, fully qualified name 360 ('[(cpp) namespace: "::const"]\n', 'const'), 361 # First component 362 ('[(cpp) namespace: "if::valid"]\n', 'if'), 363 # First component, fully qualified name 364 ('[(cpp) namespace: "::auto::pilot"]\n', 'auto'), 365 # Last component 366 ('[(cpp) namespace: "make::do"]\n', 'do'), 367 # Middle component 368 ('[(cpp) namespace: "our::new::product"]\n', 'new'), 369 ]: 370 ir = _make_ir_from_emb(test) 371 attr = ir.module[0].attribute[0] 372 373 self.assertEqual([[ 374 error.error("m.emb", attr.value.source_location, 375 f'Reserved word "{expected}" is not allowed ' 376 f'as a namespace component.')]], 377 header_generator.generate_header(ir)[1]) 378 379 380if __name__ == "__main__": 381 unittest.main() 382