Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 99 additions & 16 deletions src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,14 @@ static CharacterPropertyResolver.Result resolveJoniProperty(
property = normalizePerlIsPropertyAssignment(property);
int assignment = propertyValueDelimiter(property);
if (assignment <= 0 || assignment == property.length() - 1) {
return null;
String looseIsValue = looseIsShortcutValue(property);
if (looseIsValue == null
|| PerlUnicodeScriptData.canonicalValue(looseIsValue) != null
|| PerlUnicodeBlockData.set(looseIsValue) != null) {
return null;
}
UnicodeSet bareSet = resolvePerlBuiltInPropertyAlias(property);
return bareSet == null ? null : joniPropertyResult(bareSet, true);
}
String name = property.substring(0, assignment);
String value = property.substring(assignment + 1);
Expand Down Expand Up @@ -1033,6 +1040,11 @@ static CharacterPropertyResolver.Result resolveJoniProperty(
UnicodeSet set = resolvePerlBuiltInPropertyAlias(property);
if (set == null) return null;

return joniPropertyResult(set, caseFold);
}

private static CharacterPropertyResolver.Result joniPropertyResult(
UnicodeSet set, boolean caseFold) {
int[] ranges = new int[set.getRangeCount() * 2 + 1];
ranges[0] = set.getRangeCount();
for (int i = 0; i < set.getRangeCount(); i++) {
Expand Down Expand Up @@ -1232,6 +1244,19 @@ && loosePropertyName(alias.substring(0, assignment))
}
}
}
String looseIsValue = looseIsShortcutValue(alias);
boolean inheritedBareIs = looseIsValue != null
&& PerlUnicodeScriptData.canonicalValue(looseIsValue) == null
&& PerlUnicodeBlockData.set(looseIsValue) == null;
if (inheritedBareIs) {
// Keep General_Category ahead of binary aliases in the shared Is
// shortcut namespace, before Block's ambiguity guard runs.
UnicodeSet category = PerlUnicodeGeneralCategoryData.resolve(looseIsValue);
if (category != null) return category;
if (isIcuBinaryPropertyAlias(looseIsValue)) {
return new UnicodeSet().applyPropertyAlias(looseIsValue, "True");
}
}
if (alias.equalsIgnoreCase("L&")) {
UnicodeSet casedLetters = unicodePropertyValueSet(
UProperty.GENERAL_CATEGORY, "UppercaseLetter");
Expand All @@ -1245,8 +1270,16 @@ && loosePropertyName(alias.substring(0, assignment))
// Perl's bare script-value shortcuts use Script_Extensions semantics.
// Keep binary and General_Category names ahead of this value namespace.
String scriptShortcut = alias;
if (alias.length() > 2 && alias.startsWith("Is")) {
int valueStart = 2;
int scriptPrefixStart = 0;
while (scriptPrefixStart < alias.length()) {
char separator = alias.charAt(scriptPrefixStart);
if (!Character.isWhitespace(separator)
&& separator != '-' && separator != '_') break;
scriptPrefixStart++;
}
if (alias.length() - scriptPrefixStart > 2
&& alias.regionMatches(true, scriptPrefixStart, "is", 0, 2)) {
int valueStart = scriptPrefixStart + 2;
while (valueStart < alias.length()) {
char separator = alias.charAt(valueStart);
if (!Character.isWhitespace(separator)
Expand All @@ -1266,31 +1299,50 @@ && loosePropertyName(alias.substring(0, assignment))
}
}

String blockAlias = alias;
String blockShortcutAlias = alias;
if (assignment < 0) {
int prefixStart = 0;
while (prefixStart < alias.length()) {
char separator = alias.charAt(prefixStart);
if (!Character.isWhitespace(separator)
&& separator != '-' && separator != '_') break;
prefixStart++;
}
if (prefixStart > 0 && alias.length() - prefixStart > 2
&& (alias.regionMatches(true, prefixStart, "in", 0, 2)
|| alias.regionMatches(true, prefixStart, "is", 0, 2))) {
blockShortcutAlias = alias.substring(prefixStart);
}
}

String blockAlias = blockShortcutAlias;
boolean blockShortcut = false;
boolean isBlockShortcut = false;
if (alias.length() > 2 && alias.regionMatches(true, 0, "in", 0, 2)) {
if (blockShortcutAlias.length() > 2
&& blockShortcutAlias.regionMatches(true, 0, "in", 0, 2)) {
int valueStart = 2;
while (valueStart < alias.length()) {
char separator = alias.charAt(valueStart);
while (valueStart < blockShortcutAlias.length()) {
char separator = blockShortcutAlias.charAt(valueStart);
if (!Character.isWhitespace(separator) && separator != '-' && separator != '_') break;
valueStart++;
}
if (valueStart >= alias.length()) return null;
blockAlias = alias.substring(valueStart);
if (valueStart >= blockShortcutAlias.length()) return null;
blockAlias = blockShortcutAlias.substring(valueStart);
blockShortcut = true;
} else if (assignment >= 0 || unicodePropertyValue(UProperty.SCRIPT, alias) >= 0) {
} else if (assignment >= 0
|| unicodePropertyValue(UProperty.SCRIPT, blockShortcutAlias) >= 0) {
return null;
} else if (alias.length() > 2 && alias.regionMatches(true, 0, "is", 0, 2)) {
} else if (blockShortcutAlias.length() > 2
&& blockShortcutAlias.regionMatches(true, 0, "is", 0, 2)) {
int valueStart = 2;
while (valueStart < alias.length()) {
char separator = alias.charAt(valueStart);
while (valueStart < blockShortcutAlias.length()) {
char separator = blockShortcutAlias.charAt(valueStart);
if (!Character.isWhitespace(separator)
&& separator != '-' && separator != '_') break;
valueStart++;
}
if (valueStart >= alias.length()) return null;
String candidate = alias.substring(valueStart);
if (valueStart >= blockShortcutAlias.length()) return null;
String candidate = blockShortcutAlias.substring(valueStart);
if (unicodePropertyValue(UProperty.SCRIPT, candidate) >= 0) return null;
if (isIcuBinaryPropertyAlias(candidate)
|| isIcuGeneralCategoryAlias(candidate)) return null;
Expand All @@ -1307,7 +1359,15 @@ && loosePropertyName(alias.substring(0, assignment))
// representation debt is closed; explicit Block=/In forms remain pinned.
return null;
}
return block;
if (block != null) return block;

if (inheritedBareIs) {
// Only inherit aliases whose unprefixed spelling already resolves.
// This preserves user-property lookup and leaves missing bare bases
// (for example All and Unicode) for their owning property slices.
return resolveStandardPropertyAsSet(looseIsValue, new LinkedHashSet<>());
}
return null;
}

private static Boolean perlBooleanPropertyValue(String value) {
Expand Down Expand Up @@ -1336,6 +1396,29 @@ private static boolean isIcuGeneralCategoryAlias(String alias) {
}
}

private static String looseIsShortcutValue(String property) {
if (propertyValueDelimiter(property) >= 0) return null;
int prefixStart = 0;
while (prefixStart < property.length()) {
char separator = property.charAt(prefixStart);
if (!Character.isWhitespace(separator)
&& separator != '-' && separator != '_') break;
prefixStart++;
}
if (property.length() - prefixStart <= 2
|| !property.regionMatches(true, prefixStart, "is", 0, 2)) {
return null;
}
int valueStart = prefixStart + 2;
while (valueStart < property.length()) {
char separator = property.charAt(valueStart);
if (!Character.isWhitespace(separator)
&& separator != '-' && separator != '_') break;
valueStart++;
}
return valueStart < property.length() ? property.substring(valueStart) : null;
}

private static boolean isPerlIsPrefixedNumericWildcard(String property) {
int assignment = propertyValueDelimiter(property);
if (assignment <= 0 || assignment == property.length() - 1) return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use strict;
use warnings;
use utf8;
use Test::More tests => 12;

ok("\x{1E9E}" !~ /^[^\x{00DF}]$/iaa,
'aa negative class excludes capital sharp s sibling');
ok("\x{00DF}" !~ /^[^\x{1E9E}]$/iaa,
'aa negative class excludes lowercase sharp s sibling');
ok("\x{FB06}" !~ /^[^\x{FB05}]$/iaa,
'aa negative class excludes second st ligature sibling');
ok("\x{FB05}" !~ /^[^\x{FB06}]$/iaa,
'aa negative class excludes first st ligature sibling');

ok("s" =~ /^[^\x{00DF}]$/iaa,
'aa negative sharp s class retains ASCII s');
ok("ä" =~ /^[^\x{00DF}]$/iaa,
'aa negative sharp s class retains unrelated non-ASCII');
ok("s" =~ /^[^\x{FB05}]$/iaa,
'aa negative ligature class retains ASCII s');
ok("ä" =~ /^[^\x{FB05}]$/iaa,
'aa negative ligature class retains unrelated non-ASCII');

ok("x\x{1E9E}y" !~ /^x(?iaa:[^\x{00DF}])y$/u,
'scoped aa negative class excludes sharp s sibling');
ok("xäy" =~ /^x(?iaa:[^\x{00DF}])y$/u,
'scoped aa negative class retains unrelated member');
ok("_\x{FB06}_" !~ /^_(?iaa:[^\x{FB05}])_$/u,
'anchored aa negative class excludes ligature sibling');
ok("_ä_" =~ /^_(?iaa:[^\x{FB05}])_$/u,
'anchored aa negative class retains unrelated member');
78 changes: 78 additions & 0 deletions src/test/resources/unit/regex/unicode_binary_leading_shortcuts.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use strict;
use warnings;
use Test::More;

sub compile_property {
my ($property) = @_;
my $pattern = eval 'qr/\A\p{' . $property . '}\z/u';
return ($pattern, $@);
}

my ($canonical, $canonical_error) = compile_property('IsUppercase');
ok(defined $canonical, 'canonical Is binary shortcut compiles')
or diag($canonical_error);
like('A', $canonical, 'canonical Is binary shortcut matches a member');
unlike('a', $canonical, 'canonical Is binary shortcut excludes a nonmember');

my ($lowercase, $lowercase_error) = compile_property('isuppercase');
ok(defined $lowercase, 'binary shortcut accepts a lowercase Is prefix')
or diag($lowercase_error);
like('A', $lowercase, 'lowercase Is binary shortcut matches a member');

my ($leading, $leading_error) = compile_property('__Is_uppercase');
ok(defined $leading, 'binary shortcut accepts leading loose separators')
or diag($leading_error);
like('A', $leading, 'leading-loose binary shortcut matches a member');
unlike('a', $leading, 'leading-loose binary shortcut excludes a nonmember');

my ($hex, $hex_error) = compile_property('_ is_ASCII_HEX_DIGIT');
ok(defined $hex, 'short binary alias accepts fully loose Is spelling')
or diag($hex_error);
like('F', $hex, 'loose binary alias matches its member');
unlike('G', $hex, 'loose binary alias excludes a nonmember');

my ($assigned, $assigned_error) = compile_property('- Is_Assigned');
ok(defined $assigned, 'another binary family accepts a leading-loose Is')
or diag($assigned_error);
like('A', $assigned, 'Assigned shortcut matches an assigned character');
unlike(chr(0x0378), $assigned,
'Assigned shortcut excludes an unassigned character');

my ($script, $script_error) = compile_property('_ is_Latn');
ok(defined $script, 'Script shortcut retains precedence') or diag($script_error);
like('A', $script, 'Script shortcut membership remains intact');
unlike(chr(0x03B1), $script, 'Script shortcut still excludes another script');

my ($category, $category_error) = compile_property('IsL');
ok(defined $category, 'General_Category retains precedence')
or diag($category_error);
like(chr(0x03B1), $category,
'General_Category shortcut still matches non-Latin letters');

my ($block, $block_error) = compile_property('_ Is_Basic_Latin');
ok(defined $block, 'Block shortcut retains precedence') or diag($block_error);
like('A', $block, 'Block shortcut membership remains intact');

my $callback_calls = 0;
sub IsBinaryShortcutCallback {
$callback_calls++;
return "0041";
}
my ($callback, $callback_error) = compile_property('IsBinaryShortcutCallback');
ok(defined $callback, 'exact user property callback retains precedence')
or diag($callback_error);
like('A', $callback, 'exact user property callback matches its range');
is($callback_calls, 1, 'exact user property callback is called once');

my ($leading_callback, $leading_callback_error) =
compile_property('_ IsBinaryShortcutCallback');
ok(!defined($leading_callback) && length($leading_callback_error),
'leading separators do not broaden user property lookup');
is($callback_calls, 1, 'rejected leading callback spelling is not invoked');

my ($unknown, $unknown_error) =
compile_property('_ Is_DefinitelyNotABinaryProperty');
ok(!defined($unknown) && length($unknown_error),
'unknown leading-loose binary shortcut is rejected');

done_testing;
65 changes: 65 additions & 0 deletions src/test/resources/unit/regex/unicode_block_leading_shortcuts.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use strict;
use warnings;
use Test::More;

sub compile_property {
my ($property) = @_;
my $pattern = eval 'qr/\A\p{' . $property . '}\z/u';
return ($pattern, $@);
}

my ($canonical, $canonical_error) = compile_property('In_Emoticons');
ok(defined $canonical, 'canonical In Block shortcut compiles')
or diag($canonical_error);
like(chr(0x1F600), $canonical, 'canonical In Block shortcut matches its block');
unlike('A', $canonical, 'canonical In Block shortcut excludes another block');

my ($leading_in, $leading_in_error) =
compile_property(' _ IN_Emoticons');
ok(defined $leading_in, 'In Block shortcut accepts leading loose separators')
or diag($leading_in_error);
like(chr(0x1F600), $leading_in,
'leading-loose In Block shortcut matches its block');
unlike('A', $leading_in,
'leading-loose In Block shortcut excludes another block');

my ($leading_is, $leading_is_error) =
compile_property('- Is_Emoticons');
ok(defined $leading_is, 'Is Block shortcut accepts leading loose separators')
or diag($leading_is_error);
like(chr(0x1F600), $leading_is,
'leading-loose Is Block shortcut matches its block');
unlike('A', $leading_is,
'leading-loose Is Block shortcut excludes another block');

my ($script, $script_error) = compile_property('Is_Latin');
ok(defined $script, 'Script shortcut retains precedence') or diag($script_error);
like('A', $script, 'Script shortcut membership remains intact');

my ($binary, $binary_error) = compile_property('Is_Uppercase');
ok(defined $binary, 'binary shortcut retains precedence') or diag($binary_error);
like('A', $binary, 'binary shortcut membership remains intact');

my $callback_calls = 0;
sub Is_BlockShortcutCallback {
$callback_calls++;
return "0041";
}
my ($callback, $callback_error) =
compile_property('Is_BlockShortcutCallback');
ok(defined $callback, 'exact user property callback retains precedence')
or diag($callback_error);
like('A', $callback, 'exact user property callback matches its range');
is($callback_calls, 1, 'exact user property callback is called once');

my ($leading_callback, $leading_callback_error) =
compile_property('_ Is_BlockShortcutCallback');
ok(!defined($leading_callback) && length($leading_callback_error),
'leading separators do not broaden user property lookup');
is($callback_calls, 1, 'rejected leading callback spelling is not invoked');

my ($unknown, $unknown_error) = compile_property('_ In_DefinitelyNotABlock');
ok(!defined($unknown) && length($unknown_error),
'unknown leading-loose Block shortcut is rejected');

done_testing;
Loading