@@ -1065,6 +1065,119 @@ def test_invalid_utf8_byte_at_top_level_raises
10651065 end
10661066 end
10671067
1068+ def test_non_ascii_identifiers
1069+ RBS ::Parser . parse_signature ( buffer ( <<~RBS ) ) . tap do |_ , _ , decls |
1070+ class ServicioÚltimaVez
1071+ end
1072+
1073+ module MiMódulo
1074+ MI_CONSTANTE_Ñ: Integer
1075+ def enviar_últimas_interacciones: () -> void
1076+ def 日本語: () -> void
1077+ def únicos!: () -> void
1078+
1079+ def con_parametros: (Integer 引数, キーワード: String) -> void
1080+
1081+ @日本語: Integer
1082+ @@クラス変数: Integer
1083+
1084+ attr_reader nombre_único: String
1085+ end
1086+ RBS
1087+
1088+ assert_equal RBS ::TypeName . parse ( "ServicioÚltimaVez" ) , decls [ 0 ] . name
1089+ assert_equal RBS ::TypeName . parse ( "MiMódulo" ) , decls [ 1 ] . name
1090+
1091+ module_members = decls [ 1 ] . members
1092+ assert_equal RBS ::TypeName . parse ( "MI_CONSTANTE_Ñ" ) , module_members [ 0 ] . name
1093+ assert_equal :enviar_últimas_interacciones , module_members [ 1 ] . name
1094+ assert_equal :日 本語 , module_members [ 2 ] . name
1095+ assert_equal :"únicos!" , module_members [ 3 ] . name
1096+
1097+ function = module_members [ 4 ] . overloads [ 0 ] . method_type . type
1098+ assert_equal :引数 , function . required_positionals [ 0 ] . name
1099+ assert_equal :キーワード , function . required_keywords . keys [ 0 ]
1100+
1101+ assert_equal :@日 本語 , module_members [ 5 ] . name
1102+ assert_equal :@@クラス 変数 , module_members [ 6 ] . name
1103+ assert_equal :nombre_único , module_members [ 7 ] . name
1104+ end
1105+
1106+ RBS ::Parser . parse_signature ( buffer ( <<~RBS ) ) . tap do |_ , _ , decls |
1107+ interface _Unicós
1108+ def foo: () -> void
1109+ end
1110+
1111+ type nombre_único = Integer
1112+
1113+ $グローバル: Integer
1114+ RBS
1115+
1116+ assert_equal RBS ::TypeName . parse ( "_Unicós" ) , decls [ 0 ] . name
1117+ assert_equal RBS ::TypeName . parse ( "nombre_único" ) , decls [ 1 ] . name
1118+ assert_equal :$グローバル , decls [ 2 ] . name
1119+ end
1120+ end
1121+
1122+ def test_type_name_must_start_with_ascii
1123+ # RBS reads the case of the first character to tell a class name from an
1124+ # interface name from an alias name, and outside ASCII there is no reading
1125+ # of it that both the lexer and `TypeName#kind` can agree on. So the first
1126+ # character of a name in one of those positions has to be ASCII. Ruby asks
1127+ # for no such thing -- every one of these opens a constant or a local
1128+ # variable there -- and this is where RBS takes less than Ruby gives.
1129+ [ "日本語" , "Ωmega" , "DžFoo" ] . each do |name |
1130+ [
1131+ "class #{ name } \n end\n " ,
1132+ "module #{ name } \n end\n " ,
1133+ "interface _#{ name } \n end\n " ,
1134+ "type #{ name } = Integer\n " ,
1135+ "#{ name } : Integer\n " ,
1136+ "class Foo[#{ name } ]\n end\n " ,
1137+ "class Foo\n def f: () -> #{ name } \n end\n " ,
1138+ ] . each do |source |
1139+ assert_raises ( RBS ::ParsingError , source ) do
1140+ RBS ::Parser . parse_signature ( buffer ( source ) )
1141+ end
1142+ end
1143+ end
1144+ end
1145+
1146+ def test_non_ascii_identifier_in_single_byte_encoding
1147+ # In ISO-8859-1 this is one byte: `A` with a grave accent is 0xC0. The rule
1148+ # is about the character being outside ASCII, not about it taking more than
1149+ # one byte, so a single-byte encoding reaches it the same way.
1150+ inside = "class Abc\xC0 \n end\n " . dup . force_encoding ( Encoding ::ISO_8859_1 )
1151+ _ , _ , decls = RBS ::Parser . parse_signature ( buffer ( inside ) )
1152+ assert_equal "Abc\xC0 " . b , decls [ 0 ] . name . name . to_s . b
1153+
1154+ leading = "class \xC0 bc\n end\n " . dup . force_encoding ( Encoding ::ISO_8859_1 )
1155+ assert_raises ( RBS ::ParsingError ) do
1156+ RBS ::Parser . parse_signature ( buffer ( leading ) )
1157+ end
1158+
1159+ method_name = "class Foo\n def \xC0 bc: () -> void\n end\n " . dup . force_encoding ( Encoding ::ISO_8859_1 )
1160+ _ , _ , decls = RBS ::Parser . parse_signature ( buffer ( method_name ) )
1161+ assert_equal "\xC0 bc" . b , decls [ 0 ] . members [ 0 ] . name . to_s . b
1162+ end
1163+
1164+ def test_high_byte_in_ascii_8bit_is_an_identifier
1165+ # Every byte is a character in ASCII-8BIT, and Ruby takes any non-ASCII one
1166+ # into an identifier: `\x80abc = 1` assigns a local variable there. None of
1167+ # those bytes is ASCII, so a name may not open with one.
1168+ source = "class \x80 end" . dup . force_encoding ( Encoding ::ASCII_8BIT )
1169+ assert_equal :tNONASCIIIDENT , RBS ::Parser . lex ( source ) . value [ 2 ] . type
1170+
1171+ method_name = "class Foo\n def \x80 abc: () -> void\n end\n " . dup . force_encoding ( Encoding ::ASCII_8BIT )
1172+ _ , _ , decls = RBS ::Parser . parse_signature ( buffer ( method_name ) )
1173+ assert_equal "\x80 abc" . b , decls [ 0 ] . members [ 0 ] . name . to_s . b
1174+
1175+ alias_decl = "type \x80 abc = Integer\n " . dup . force_encoding ( Encoding ::ASCII_8BIT )
1176+ assert_raises ( RBS ::ParsingError ) do
1177+ RBS ::Parser . parse_signature ( buffer ( alias_decl ) )
1178+ end
1179+ end
1180+
10681181 def test_utf8_replacement_character_in_comment_parses
10691182 # A genuine U+FFFD (REPLACEMENT CHARACTER) is a valid 3-byte UTF-8 sequence
10701183 # ("\xEF\xBF\xBD") that decodes to the multibyte dummy code point, not the
0 commit comments