Skip to content

Commit d46cf40

Browse files
committed
Test and document non-ASCII identifiers
Covers the names the lexer now takes -- classes, modules, constants, methods, parameters, keywords, instance and class variables, attributes, interfaces, aliases and globals -- and the line the first character draws: a name whose kind is read from it must open with ASCII, everything else need not. The negative cases run three characters through every position that reads a kind, including a titlecase letter, which Ruby does start a constant on. That is the clearest case of RBS taking less than Ruby gives, so it is worth saying in a test. ASCII-8BIT has no invalid bytes, so a high byte there is an identifier character, exactly as it is for Ruby: `\x80abc = 1` assigns a local variable. None of those bytes is ASCII, so a name may not open with one. `TypeName#kind` never sees a name that opens outside ASCII from the parser. One built by hand still can, and the test says what it reads there: nothing.
1 parent ef8e52a commit d46cf40

3 files changed

Lines changed: 138 additions & 0 deletions

File tree

docs/syntax.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ _proc_ ::= `^` _parameters?_ _self-type-binding?_ _block?_ `->` _type_
4848
| `^` `(` `?` `)` `->` _type_ # Proc type with untyped parameter
4949
```
5050

51+
`\w` above, and everywhere else in this document, is `[a-zA-Z0-9_]` together
52+
with every character outside ASCII -- the same set Ruby takes into an
53+
identifier. So `ServicioÚltimaVez` is a class name and `nombre_único` is an
54+
alias name.
55+
56+
The leading character is the exception. RBS reads it to tell a class name from
57+
an interface name from an alias name, so where it makes that distinction it has
58+
to be ASCII: `class 日本語` is a class in Ruby but not a name RBS can write.
59+
Nowhere else is restricted -- a method name, a variable name, an instance
60+
variable name and a class variable name may all open with any character Ruby
61+
accepts.
62+
5163
### Class instance type
5264

5365
Class instance type denotes _an instance of a class_.

test/rbs/parser_test.rb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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}\nend\n",
1132+
"module #{name}\nend\n",
1133+
"interface _#{name}\nend\n",
1134+
"type #{name} = Integer\n",
1135+
"#{name}: Integer\n",
1136+
"class Foo[#{name}]\nend\n",
1137+
"class Foo\n def f: () -> #{name}\nend\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\nend\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 \xC0bc\nend\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 \xC0bc: () -> void\nend\n".dup.force_encoding(Encoding::ISO_8859_1)
1160+
_, _, decls = RBS::Parser.parse_signature(buffer(method_name))
1161+
assert_equal "\xC0bc".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 \x80abc: () -> void\nend\n".dup.force_encoding(Encoding::ASCII_8BIT)
1172+
_, _, decls = RBS::Parser.parse_signature(buffer(method_name))
1173+
assert_equal "\x80abc".b, decls[0].members[0].name.to_s.b
1174+
1175+
alias_decl = "type \x80abc = 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

test/rbs/type_name_test.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,17 @@ def test_intern_preserves_kind_detection
5252
assert_equal :alias, aliased.kind
5353
assert_equal :interface, interface.kind
5454
end
55+
56+
def test_kind_reads_only_the_leading_ascii_character
57+
ns = Namespace.root
58+
59+
# The parser never hands `kind` a name that opens outside ASCII, because a
60+
# type name has to start with an ASCII character to be one. A name built by
61+
# hand still can, and `kind` reads nothing there -- no Unicode table on this
62+
# side, which is the whole point of that restriction.
63+
assert_equal :class, TypeName[ns, :Foo日本語].kind
64+
assert_equal :alias, TypeName[ns, :foo日本語].kind
65+
assert_equal :interface, TypeName[ns, :_Foo日本語].kind
66+
assert_equal :class, TypeName[ns, :日本語].kind
67+
end
5568
end

0 commit comments

Comments
 (0)