feat: fluent.syntax
Портирование кода из fluent-kotlin https://github.com/projectfluent/fluent-kotlin
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
tab_width = 4
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
gradlew text eol=lf
|
||||||
|
gradlew.bat text eol=crlf
|
||||||
|
gradle/wrapper/gradle-wrapper.properties text eol=lf
|
||||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Java
|
||||||
|
hs_err_pid*.log
|
||||||
|
|
||||||
|
# JetBrains IDEA
|
||||||
|
.idea/
|
||||||
|
!.idea/codeStyles/codeStyleConfig.xml
|
||||||
|
!.idea/codeStyles/Project.xml
|
||||||
|
!.idea/codeInsightSettings.xml
|
||||||
|
!.idea/.gitignore
|
||||||
|
*.iml
|
||||||
4
build.gradle
Normal file
4
build.gradle
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
wrapper {
|
||||||
|
gradleVersion = "8.6"
|
||||||
|
distributionType = Wrapper.DistributionType.BIN
|
||||||
|
}
|
||||||
45
fluent.syntax/build.gradle
Normal file
45
fluent.syntax/build.gradle
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
plugins {
|
||||||
|
id "java"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileJava {
|
||||||
|
targetCompatibility = sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ru.di9.fluent"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
assertjVersion = "3.24.2"
|
||||||
|
gsonVersion = "2.9.1"
|
||||||
|
joorVersion = "0.9.15"
|
||||||
|
jsonAssertVersion = "1.5.1"
|
||||||
|
junitVersion = "5.9.2"
|
||||||
|
lombokVersion = "1.18.30"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
annotationProcessor("org.projectlombok:lombok:$lombokVersion")
|
||||||
|
compileOnly("org.projectlombok:lombok:$lombokVersion")
|
||||||
|
|
||||||
|
testImplementation(platform("org.junit:junit-bom:$junitVersion"))
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
|
||||||
|
testAnnotationProcessor("org.projectlombok:lombok:$lombokVersion")
|
||||||
|
testCompileOnly("org.projectlombok:lombok:$lombokVersion")
|
||||||
|
testImplementation("org.assertj:assertj-core:$assertjVersion")
|
||||||
|
testImplementation("com.google.code.gson:gson:$gsonVersion")
|
||||||
|
testImplementation("org.jooq:joor:$joorVersion")
|
||||||
|
testImplementation("org.skyscreamer:jsonassert:$jsonAssertVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax;
|
||||||
|
|
||||||
|
public interface MathUtils {
|
||||||
|
static int clamp(int value, int min, int max) {
|
||||||
|
return Math.max(min, Math.min(value, max));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package ru.di9.fluent.syntax;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface StringUtils {
|
||||||
|
|
||||||
|
static Optional<Character> getCharAt(String string, int index) {
|
||||||
|
if (string == null || string.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0 && index <= string.length() - 1) {
|
||||||
|
return Optional.of(string.charAt(index));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean inRange_az(int cc) {
|
||||||
|
return (97 <= cc && cc <= 122); // a-z
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean inRange_AZ(int cc) {
|
||||||
|
return (65 <= cc && cc <= 90); // A-Z
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean inRange_af(int cc) {
|
||||||
|
return (97 <= cc && cc <= 102); // a-f
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean inRange_AF(int cc) {
|
||||||
|
return (65 <= cc && cc <= 70); // A-F
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean inRange_09(int cc) {
|
||||||
|
return (48 <= cc && cc <= 57); // 0-9
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Annotation extends SyntaxNode {
|
||||||
|
private final List<Object> arguments = new ArrayList<>();
|
||||||
|
private final String code;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Annotation that = (Annotation) o;
|
||||||
|
return Objects.equals(code, that.code) &&
|
||||||
|
Objects.equals(message, that.message) &&
|
||||||
|
Objects.equals(arguments, that.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(code, message, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Attribute extends SyntaxNode {
|
||||||
|
private final Identifier id;
|
||||||
|
private final Pattern value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Attribute attribute = (Attribute) o;
|
||||||
|
return Objects.equals(id, attribute.id) &&
|
||||||
|
Objects.equals(value, attribute.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public abstract class BaseComment extends Entry {
|
||||||
|
private final String content;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
BaseComment that = (BaseComment) o;
|
||||||
|
return Objects.equals(content, that.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public abstract class BaseNode {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public interface CallArgument {
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class CallArguments extends SyntaxNode {
|
||||||
|
private final List<Expression> positional = new ArrayList<>();
|
||||||
|
private final List<NamedArgument> named = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
CallArguments that = (CallArguments) o;
|
||||||
|
return Objects.equals(positional, that.positional) && Objects.equals(named, that.named);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(positional, named);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public class Comment extends BaseComment {
|
||||||
|
public Comment(String content) {
|
||||||
|
super(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public class Entry extends TopLevel {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public abstract class Expression extends SyntaxNode implements CallArgument, InsidePlaceable {
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class FunctionReference extends Expression {
|
||||||
|
private final Identifier id;
|
||||||
|
private final CallArguments arguments;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
FunctionReference that = (FunctionReference) o;
|
||||||
|
return Objects.equals(id, that.id) &&
|
||||||
|
Objects.equals(arguments, that.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public class GroupComment extends BaseComment {
|
||||||
|
public GroupComment(String content) {
|
||||||
|
super(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@ToString(of = {"name"})
|
||||||
|
public class Identifier extends SyntaxNode implements VariantKey {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Identifier that = (Identifier) o;
|
||||||
|
return Objects.equals(name, that.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public interface InsidePlaceable {
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Junk extends TopLevel {
|
||||||
|
private final List<Annotation> annotations = new ArrayList<>();
|
||||||
|
private final String content;
|
||||||
|
|
||||||
|
public void addAnnotation(Annotation annotation) {
|
||||||
|
this.getAnnotations().add(annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Junk junk = (Junk) o;
|
||||||
|
return Objects.equals(content, junk.content) &&
|
||||||
|
Objects.equals(annotations, junk.annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(content, annotations);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public abstract class Literal extends Expression {
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Literal literal = (Literal) o;
|
||||||
|
return Objects.equals(value, literal.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class Message extends Entry {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final List<Attribute> attributes = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Identifier id;
|
||||||
|
|
||||||
|
private final Pattern value;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
private Comment comment;
|
||||||
|
|
||||||
|
public Optional<Comment> getComment() {
|
||||||
|
return Optional.ofNullable(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Pattern> getValue() {
|
||||||
|
return Optional.ofNullable(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Message message = (Message) o;
|
||||||
|
return Objects.equals(id, message.id) &&
|
||||||
|
Objects.equals(value, message.value) &&
|
||||||
|
Objects.equals(attributes, message.attributes) &&
|
||||||
|
Objects.equals(comment, message.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id, value, attributes, comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageReference extends Expression {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Identifier id;
|
||||||
|
|
||||||
|
private final Identifier attribute;
|
||||||
|
|
||||||
|
public Optional<Identifier> getAttribute() {
|
||||||
|
return Optional.ofNullable(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
MessageReference that = (MessageReference) o;
|
||||||
|
return Objects.equals(id, that.id) &&
|
||||||
|
Objects.equals(attribute, that.attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id, attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class NamedArgument extends SyntaxNode implements CallArgument {
|
||||||
|
private final Identifier name;
|
||||||
|
private final Literal value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
NamedArgument that = (NamedArgument) o;
|
||||||
|
return Objects.equals(name, that.name) &&
|
||||||
|
Objects.equals(value, that.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public class NumberLiteral extends Literal implements VariantKey {
|
||||||
|
public NumberLiteral(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class Pattern extends SyntaxNode {
|
||||||
|
private final List<PatternElement> elements;
|
||||||
|
|
||||||
|
public Pattern() {
|
||||||
|
this.elements = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pattern(PatternElement... elements) {
|
||||||
|
this.elements = new ArrayList<>(Arrays.asList(elements));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pattern(List<PatternElement> elements) {
|
||||||
|
this.elements = elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
var that = (Pattern) o;
|
||||||
|
return Objects.equals(this.elements, that.elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public abstract class PatternElement extends SyntaxNode {
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Placeable extends PatternElement implements InsidePlaceable {
|
||||||
|
private final InsidePlaceable expression;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Placeable placeable = (Placeable) o;
|
||||||
|
return Objects.equals(expression, placeable.expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(expression);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class Resource extends SyntaxNode {
|
||||||
|
private final List<TopLevel> body = new ArrayList<>();
|
||||||
|
|
||||||
|
public Resource(List<TopLevel> children) {
|
||||||
|
this.body.addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Resource resource = (Resource) o;
|
||||||
|
return Objects.equals(body, resource.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public class ResourceComment extends BaseComment {
|
||||||
|
public ResourceComment(String content) {
|
||||||
|
super(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class SelectExpression extends Expression {
|
||||||
|
private final Expression selector;
|
||||||
|
private final List<Variant> variants;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
SelectExpression that = (SelectExpression) o;
|
||||||
|
return Objects.equals(selector, that.selector) &&
|
||||||
|
Objects.equals(variants, that.variants);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(selector, variants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Span extends BaseNode {
|
||||||
|
private int start;
|
||||||
|
private int end;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Span span = (Span) o;
|
||||||
|
return start == span.start && end == span.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public class StringLiteral extends Literal {
|
||||||
|
public StringLiteral(String value) {
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class SyntaxNode extends BaseNode {
|
||||||
|
private Span span;
|
||||||
|
|
||||||
|
public Optional<Span> getSpan() {
|
||||||
|
return Optional.ofNullable(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSpan(int start, int end) {
|
||||||
|
span = new Span(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
SyntaxNode that = (SyntaxNode) o;
|
||||||
|
return Objects.equals(span, that.span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class Term extends Entry {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final List<Attribute> attributes = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Identifier id;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Pattern value;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
private Comment comment;
|
||||||
|
|
||||||
|
public Optional<Comment> getComment() {
|
||||||
|
return Optional.ofNullable(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Term term = (Term) o;
|
||||||
|
return Objects.equals(id, term.id) && Objects.equals(value, term.value) && Objects.equals(attributes, term.attributes) && Objects.equals(comment, term.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), id, value, attributes, comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TermReference extends Expression {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Identifier id;
|
||||||
|
|
||||||
|
private final Identifier attribute;
|
||||||
|
|
||||||
|
private final CallArguments arguments;
|
||||||
|
|
||||||
|
public Optional<Identifier> getAttribute() {
|
||||||
|
return Optional.ofNullable(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<CallArguments> getArguments() {
|
||||||
|
return Optional.ofNullable(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
TermReference that = (TermReference) o;
|
||||||
|
return Objects.equals(id, that.id) &&
|
||||||
|
Objects.equals(attribute, that.attribute) &&
|
||||||
|
Objects.equals(arguments, that.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), id, attribute, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class TextElement extends PatternElement {
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
TextElement that = (TextElement) o;
|
||||||
|
return Objects.equals(value, that.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public abstract class TopLevel extends SyntaxNode {
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class VariableReference extends Expression {
|
||||||
|
private final Identifier id;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
VariableReference that = (VariableReference) o;
|
||||||
|
return Objects.equals(id, that.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Variant extends SyntaxNode {
|
||||||
|
private final VariantKey key;
|
||||||
|
private final Pattern value;
|
||||||
|
private final boolean isDefault;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Variant variant = (Variant) o;
|
||||||
|
return Objects.equals(key, variant.key) &&
|
||||||
|
Objects.equals(value, variant.value) &&
|
||||||
|
Objects.equals(isDefault, variant.isDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), key, value, isDefault);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
public interface VariantKey {
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class Whitespace extends TopLevel {
|
||||||
|
private final String content;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
if (!super.equals(o)) return false;
|
||||||
|
Whitespace that = (Whitespace) o;
|
||||||
|
return Objects.equals(content, that.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,869 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import ru.di9.fluent.syntax.ast.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import static ru.di9.fluent.syntax.StringUtils.inRange_09;
|
||||||
|
import static ru.di9.fluent.syntax.parser.FluentStream.*;
|
||||||
|
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.*;
|
||||||
|
|
||||||
|
public class FluentParser {
|
||||||
|
public boolean withSpans = false;
|
||||||
|
public boolean withJunkAnnotations = true;
|
||||||
|
|
||||||
|
public Resource parse(String source) {
|
||||||
|
var ps = new FluentStream(source);
|
||||||
|
int spanStart = ps.index;
|
||||||
|
List<TopLevel> entries = new ArrayList<>();
|
||||||
|
Comment lastComment = null;
|
||||||
|
var blankLines = ps.skipBlankBlock();
|
||||||
|
if (!blankLines.isEmpty()) {
|
||||||
|
entries.add(new Whitespace(blankLines));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ps.currentChar().isPresent()) {
|
||||||
|
var entry = getEntryOrJunk(ps);
|
||||||
|
blankLines = ps.skipBlankBlock();
|
||||||
|
|
||||||
|
// Regular Comments require special logic. Comments may be attached to
|
||||||
|
// Messages or Terms if they are followed immediately by them. However
|
||||||
|
// they should parse as standalone when they're followed by Junk.
|
||||||
|
// Consequently, we only attach Comments once we know that the Message
|
||||||
|
// or the Term parsed successfully.
|
||||||
|
if (entry instanceof Comment comment && blankLines.isEmpty() && ps.currentChar().isPresent()) {
|
||||||
|
// Stash the comment and decide what to do with it in the next pass.
|
||||||
|
lastComment = comment;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastComment != null) {
|
||||||
|
if (entry instanceof Message message) {
|
||||||
|
message.setComment(lastComment);
|
||||||
|
if (withSpans && message.getSpan().isPresent() && lastComment.getSpan().isPresent()) {
|
||||||
|
message.getSpan().get().setStart(lastComment.getSpan().get().getStart());
|
||||||
|
}
|
||||||
|
} else if (entry instanceof Term term) {
|
||||||
|
term.setComment(lastComment);
|
||||||
|
if (withSpans && term.getSpan().isPresent() && lastComment.getSpan().isPresent()) {
|
||||||
|
term.getSpan().get().setStart(lastComment.getSpan().get().getStart());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries.add(lastComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In either case, the stashed comment has been dealt with; clear it.
|
||||||
|
lastComment = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No special logic for other types of entries.
|
||||||
|
entries.add(entry);
|
||||||
|
if (!blankLines.isEmpty()) {
|
||||||
|
entries.add(new Whitespace(blankLines));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resource = new Resource(entries);
|
||||||
|
if (withSpans) {
|
||||||
|
resource.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private TopLevel getEntryOrJunk(FluentStream ps) {
|
||||||
|
int entryStartPos = ps.index;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var entry = getEntry(ps);
|
||||||
|
ps.expectLineEnd();
|
||||||
|
return entry;
|
||||||
|
} catch (ParseException e) {
|
||||||
|
int errorIndex = ps.index;
|
||||||
|
ps.skipToNextEntryStart(entryStartPos);
|
||||||
|
int nextEntryStart = ps.index;
|
||||||
|
if (nextEntryStart < errorIndex) {
|
||||||
|
// The position of the error must be inside of the Junk's span.
|
||||||
|
errorIndex = nextEntryStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Junk instance
|
||||||
|
var slice = ps.string.substring(entryStartPos, nextEntryStart);
|
||||||
|
var junk = new Junk(slice);
|
||||||
|
if (withSpans) {
|
||||||
|
junk.addSpan(entryStartPos, nextEntryStart);
|
||||||
|
}
|
||||||
|
if (withJunkAnnotations) {
|
||||||
|
var annot = new Annotation(e.getCode().name(), e.getMessage());
|
||||||
|
annot.getArguments().addAll(Arrays.asList(e.getArgs()));
|
||||||
|
annot.addSpan(errorIndex, errorIndex);
|
||||||
|
junk.addAnnotation(annot);
|
||||||
|
}
|
||||||
|
return junk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Entry getEntry(FluentStream ps) {
|
||||||
|
var currentChar = ps.currentChar();
|
||||||
|
|
||||||
|
if (currentChar.filter(v -> v == '#').isPresent()) {
|
||||||
|
return getComment(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChar.filter(v -> v == '-').isPresent()) {
|
||||||
|
return getTerm(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.isIdentifierStart()) {
|
||||||
|
return getMessage(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(E0002);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Message getMessage(FluentStream ps) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
var id = getIdentifier(ps);
|
||||||
|
|
||||||
|
ps.skipBlankInline();
|
||||||
|
ps.expectChar('=');
|
||||||
|
|
||||||
|
var value = maybeGetPattern(ps);
|
||||||
|
var attrs = getAttributes(ps);
|
||||||
|
|
||||||
|
if (value == null && attrs.isEmpty()) {
|
||||||
|
throw new ParseException(E0005, id.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = new Message(id, value);
|
||||||
|
msg.getAttributes().addAll(attrs);
|
||||||
|
if (withSpans) {
|
||||||
|
msg.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Term getTerm(FluentStream ps) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
|
||||||
|
ps.expectChar('-');
|
||||||
|
var id = getIdentifier(ps);
|
||||||
|
|
||||||
|
ps.skipBlankInline();
|
||||||
|
ps.expectChar('=');
|
||||||
|
|
||||||
|
var value = maybeGetPattern(ps);
|
||||||
|
if (value == null) {
|
||||||
|
throw new ParseException(E0006, id.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
var attrs = getAttributes(ps);
|
||||||
|
var term = new Term(id, value);
|
||||||
|
term.getAttributes().addAll(attrs);
|
||||||
|
|
||||||
|
if (withSpans) {
|
||||||
|
term.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return term;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<Attribute> getAttributes(FluentStream ps) {
|
||||||
|
List<Attribute> attrs = new ArrayList<>();
|
||||||
|
ps.peekBlank();
|
||||||
|
while (ps.isAttributeStart()) {
|
||||||
|
ps.skipToPeek();
|
||||||
|
var attr = getAttribute(ps);
|
||||||
|
attrs.add(attr);
|
||||||
|
ps.peekBlank();
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Attribute getAttribute(FluentStream ps) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
ps.expectChar('.');
|
||||||
|
|
||||||
|
var key = getIdentifier(ps);
|
||||||
|
|
||||||
|
ps.skipBlankInline();
|
||||||
|
ps.expectChar('=');
|
||||||
|
|
||||||
|
var value = maybeGetPattern(ps);
|
||||||
|
if (value == null) {
|
||||||
|
throw new ParseException(E0012);
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = new Attribute(key, value);
|
||||||
|
if (withSpans) {
|
||||||
|
attribute.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeGetPattern distinguishes between patterns which start on the same line
|
||||||
|
// as the identifier (a.k.a. inline signleline patterns and inline multiline
|
||||||
|
// patterns) and patterns which start on a new line (a.k.a. block multiline
|
||||||
|
// patterns). The distinction is important for the dedentation logic: the
|
||||||
|
// indent of the first line of a block pattern must be taken into account when
|
||||||
|
// calculating the maximum common indent.
|
||||||
|
private Pattern maybeGetPattern(FluentStream ps) {
|
||||||
|
ps.peekBlankInline();
|
||||||
|
if (ps.isValueStart()) {
|
||||||
|
ps.skipToPeek();
|
||||||
|
return getPattern(ps, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.peekBlankBlock();
|
||||||
|
if (ps.isValueContinuation()) {
|
||||||
|
ps.skipToPeek();
|
||||||
|
return getPattern(ps, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pattern getPattern(FluentStream ps, boolean isBlock) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
List<PatternElement> elements = new ArrayList<>();
|
||||||
|
int commonIndentLength = Integer.MAX_VALUE;
|
||||||
|
if (isBlock) {
|
||||||
|
// A block pattern is a pattern which starts on a new line. Store and
|
||||||
|
// measure the indent of this first line for the dedentation logic.
|
||||||
|
var blankStart = ps.index;
|
||||||
|
var firstIndent = ps.skipBlankInline();
|
||||||
|
elements.add(getIndent(ps, firstIndent, blankStart));
|
||||||
|
commonIndentLength = firstIndent.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<Character> opt;
|
||||||
|
elements:
|
||||||
|
while ((opt = ps.currentChar()).isPresent()) {
|
||||||
|
var ch = opt.get();
|
||||||
|
switch (ch) {
|
||||||
|
case EOL -> {
|
||||||
|
var blankStart = ps.index;
|
||||||
|
var blankLines = ps.peekBlankBlock();
|
||||||
|
if (ps.isValueContinuation()) {
|
||||||
|
ps.skipToPeek();
|
||||||
|
var indent = ps.skipBlankInline();
|
||||||
|
commonIndentLength = Math.min(commonIndentLength, indent.length());
|
||||||
|
elements.add(getIndent(ps, blankLines + indent, blankStart));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The end condition for getPattern's while loop is a newline
|
||||||
|
// which is not followed by a valid pattern continuation.
|
||||||
|
ps.resetPeek();
|
||||||
|
break elements;
|
||||||
|
}
|
||||||
|
case '{' -> elements.add(getPlaceable(ps));
|
||||||
|
case '}' -> throw new ParseException(E0027);
|
||||||
|
default -> elements.add(getTextElement(ps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dedented = dedent(elements, commonIndentLength);
|
||||||
|
var pattern = new Pattern(dedented);
|
||||||
|
if (withSpans) {
|
||||||
|
pattern.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedent a list of elements by removing the maximum common indent from the
|
||||||
|
// beginning of text lines. The common indent is calculated in getPattern.
|
||||||
|
private List<PatternElement> dedent(Collection<PatternElement> elements, int commonIndent) {
|
||||||
|
ArrayList<PatternElement> trimmed = new ArrayList<>();
|
||||||
|
|
||||||
|
for (var element : elements) {
|
||||||
|
if (element instanceof Placeable pl) {
|
||||||
|
trimmed.add(pl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element instanceof Indent ind) {
|
||||||
|
// Strip common indent.
|
||||||
|
ind.setValue(ind.getValue().substring(0, (ind.getValue().length() - commonIndent)));
|
||||||
|
if (ind.getValue().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
var prev = trimmed.get(trimmed.size() - 1);
|
||||||
|
if (prev instanceof TextElement prevTE) {
|
||||||
|
// Join adjacent TextElements by replacing them with their sum.
|
||||||
|
String newVal;
|
||||||
|
if (element instanceof TextElement elmTE) {
|
||||||
|
newVal = elmTE.getValue();
|
||||||
|
} else if (element instanceof Indent elmInd) {
|
||||||
|
newVal = elmInd.getValue();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unexpected PatternElement type");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = new TextElement(prevTE.getValue() + newVal);
|
||||||
|
if (withSpans && prevTE.getSpan().isPresent() && element.getSpan().isPresent()) {
|
||||||
|
sum.addSpan(prevTE.getSpan().get().getStart(), element.getSpan().get().getEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed.set(trimmed.size() - 1, sum);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element instanceof Indent elmInd) {
|
||||||
|
// If the indent hasn't been merged into a preceding TextElement,
|
||||||
|
// convert it into a new TextElement.
|
||||||
|
var textElement = new TextElement(elmInd.getValue());
|
||||||
|
if (withSpans && elmInd.getSpan().isPresent()) {
|
||||||
|
textElement.addSpan(elmInd.getSpan().get().getStart(), elmInd.getSpan().get().getEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed.add(textElement);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The element is a TextElement or a Placeable
|
||||||
|
trimmed.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim trailing whitespace from the Pattern.
|
||||||
|
var lastElement = trimmed.get(trimmed.size() - 1);
|
||||||
|
if (lastElement instanceof TextElement lastElmTE) {
|
||||||
|
lastElmTE.setValue(TRAILING_WS_RE.matcher(lastElmTE.getValue()).replaceAll(""));
|
||||||
|
if (lastElmTE.getValue().isEmpty()) {
|
||||||
|
trimmed.remove(trimmed.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextElement getTextElement(FluentStream ps) {
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
int spanStart = ps.index;
|
||||||
|
|
||||||
|
Optional<Character> opt;
|
||||||
|
while ((opt = ps.currentChar()).isPresent()) {
|
||||||
|
char ch = opt.get();
|
||||||
|
|
||||||
|
if (ch == '{' || ch == '}' || ch == EOL) {
|
||||||
|
var textElement = new TextElement(buffer.toString());
|
||||||
|
if (withSpans) {
|
||||||
|
textElement.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
return textElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.append(ch);
|
||||||
|
ps.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
var textElement = new TextElement(buffer.toString());
|
||||||
|
if (withSpans) {
|
||||||
|
textElement.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return textElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PatternElement getPlaceable(FluentStream ps) {
|
||||||
|
var spanStart = ps.index;
|
||||||
|
ps.expectChar('{');
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
SyntaxNode expression;
|
||||||
|
if (ps.currentChar().filter(v -> v == '{').isPresent()) {
|
||||||
|
var child = getPlaceable(ps);
|
||||||
|
ps.skipBlank();
|
||||||
|
expression = child;
|
||||||
|
} else {
|
||||||
|
expression = getExpression(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.expectChar('}');
|
||||||
|
|
||||||
|
var placeable = new Placeable((InsidePlaceable) expression);
|
||||||
|
if (withSpans) {
|
||||||
|
placeable.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression getExpression(FluentStream ps) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
|
||||||
|
var selector = getInlineExpression(ps);
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '-').isPresent()) {
|
||||||
|
if (ps.peek().filter(v -> v != '>').isPresent()) {
|
||||||
|
ps.resetPeek();
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate selector expression according to
|
||||||
|
// abstract.js in the Fluent specification
|
||||||
|
|
||||||
|
if (selector instanceof MessageReference mr) {
|
||||||
|
throw new ParseException(mr.getAttribute().isEmpty() ? E0016 : E0018);
|
||||||
|
} else if (selector instanceof TermReference tr && tr.getAttribute().isEmpty()) {
|
||||||
|
throw new ParseException(E0017);
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.next();
|
||||||
|
ps.next();
|
||||||
|
|
||||||
|
ps.skipBlankInline();
|
||||||
|
ps.expectLineEnd();
|
||||||
|
|
||||||
|
var variants = getVariants(ps);
|
||||||
|
|
||||||
|
var selectExpression = new SelectExpression(selector, variants);
|
||||||
|
if (withSpans) {
|
||||||
|
selectExpression.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector instanceof TermReference tr && tr.getAttribute().isPresent()) {
|
||||||
|
throw new ParseException(E0019);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Variant> getVariants(FluentStream ps) {
|
||||||
|
List<Variant> variants = new ArrayList<>();
|
||||||
|
var hasDefault = false;
|
||||||
|
|
||||||
|
ps.skipBlank();
|
||||||
|
while (ps.isVariantStart()) {
|
||||||
|
var variant = getVariant(ps, hasDefault);
|
||||||
|
|
||||||
|
if (variant.isDefault()) {
|
||||||
|
hasDefault = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
variants.add(variant);
|
||||||
|
ps.expectLineEnd();
|
||||||
|
ps.skipBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variants.isEmpty()) {
|
||||||
|
throw new ParseException(E0011);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDefault) {
|
||||||
|
throw new ParseException(E0010);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Variant getVariant(FluentStream ps, boolean hasDefault) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
var defaultIndex = false;
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '*').isPresent()) {
|
||||||
|
if (hasDefault) {
|
||||||
|
throw new ParseException(E0015);
|
||||||
|
}
|
||||||
|
ps.next();
|
||||||
|
defaultIndex = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.expectChar('[');
|
||||||
|
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
var key = getVariantKey(ps);
|
||||||
|
|
||||||
|
ps.skipBlank();
|
||||||
|
ps.expectChar(']');
|
||||||
|
|
||||||
|
//val value = this.maybeGetPattern(ps) ?: throw ParseError("E0012")
|
||||||
|
var value = maybeGetPattern(ps);
|
||||||
|
if (value == null) {
|
||||||
|
throw new ParseException(E0012);
|
||||||
|
}
|
||||||
|
|
||||||
|
var variant = new Variant(key, value, defaultIndex);
|
||||||
|
if (withSpans) {
|
||||||
|
variant.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VariantKey getVariantKey(FluentStream ps) {
|
||||||
|
int cc = ps.currentChar().orElseThrow(() -> new ParseException(E0013));
|
||||||
|
if (inRange_09(cc) || cc == 45 /*-*/) {
|
||||||
|
return getNumber(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getIdentifier(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression getInlineExpression(FluentStream ps) {
|
||||||
|
var spanStart = ps.index;
|
||||||
|
|
||||||
|
if (ps.isNumberStart()) {
|
||||||
|
return getNumber(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '"').isPresent()) {
|
||||||
|
return getString(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '$').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
var id = getIdentifier(ps);
|
||||||
|
|
||||||
|
var variableReference = new VariableReference(id);
|
||||||
|
if (withSpans) {
|
||||||
|
variableReference.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variableReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '-').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
var id = getIdentifier(ps);
|
||||||
|
|
||||||
|
Identifier attr = null;
|
||||||
|
if (ps.currentChar().filter(v -> v == '.').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
attr = getIdentifier(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
CallArguments args = null;
|
||||||
|
ps.peekBlank();
|
||||||
|
if (ps.currentPeek().filter(v -> v == '(').isPresent()) {
|
||||||
|
ps.skipToPeek();
|
||||||
|
args = getCallArguments(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
var termReference = new TermReference(id, attr, args);
|
||||||
|
if (withSpans) {
|
||||||
|
termReference.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return termReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.isIdentifierStart()) {
|
||||||
|
var id = getIdentifier(ps);
|
||||||
|
ps.peekBlank();
|
||||||
|
|
||||||
|
if (ps.currentPeek().filter(v -> v == '(').isPresent()) {
|
||||||
|
// It's a Function. Ensure it's all upper-case.
|
||||||
|
if (!VALID_FUNCTION_NAME.matcher(id.getName()).matches()) {
|
||||||
|
throw new ParseException(E0008);
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.skipToPeek();
|
||||||
|
var args = getCallArguments(ps);
|
||||||
|
var functionReference = new FunctionReference(id, args);
|
||||||
|
if (withSpans) {
|
||||||
|
functionReference.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
return functionReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
Identifier attr = null;
|
||||||
|
if (ps.currentChar().filter(v -> v == '.').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
attr = this.getIdentifier(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageReference = new MessageReference(id, attr);
|
||||||
|
if (withSpans) {
|
||||||
|
messageReference.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(E0028);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallArguments getCallArguments(FluentStream ps) {
|
||||||
|
var spanStart = ps.index;
|
||||||
|
List<Expression> positional = new ArrayList<>();
|
||||||
|
List<NamedArgument> named = new ArrayList<>();
|
||||||
|
Set<String> argumentNames = new HashSet<>();
|
||||||
|
|
||||||
|
ps.expectChar('(');
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (ps.currentChar().filter(v -> v == ')').isPresent()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arg = getCallArgument(ps);
|
||||||
|
if (arg instanceof NamedArgument na) {
|
||||||
|
if (argumentNames.contains(na.getName().getName())) {
|
||||||
|
throw new ParseException(E0022);
|
||||||
|
}
|
||||||
|
named.add(na);
|
||||||
|
argumentNames.add(na.getName().getName());
|
||||||
|
} else if (arg instanceof Expression exp) {
|
||||||
|
if (!argumentNames.isEmpty()) {
|
||||||
|
throw new ParseException(E0021);
|
||||||
|
}
|
||||||
|
positional.add(exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == ',').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
ps.skipBlank();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.expectChar(')');
|
||||||
|
var args = new CallArguments();
|
||||||
|
args.getPositional().addAll(positional);
|
||||||
|
args.getNamed().addAll(named);
|
||||||
|
if (withSpans) {
|
||||||
|
args.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallArgument getCallArgument(FluentStream ps) {
|
||||||
|
var spanStart = ps.index;
|
||||||
|
var exp = getInlineExpression(ps);
|
||||||
|
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v != ':').isPresent()) {
|
||||||
|
return exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exp instanceof MessageReference mr && mr.getAttribute().isEmpty()) {
|
||||||
|
ps.next();
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
var value = getLiteral(ps);
|
||||||
|
|
||||||
|
var namedArgument = new NamedArgument(mr.getId(), value);
|
||||||
|
if (withSpans) {
|
||||||
|
namedArgument.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return namedArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(E0009);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Literal getLiteral(FluentStream ps) {
|
||||||
|
if (ps.isNumberStart()) {
|
||||||
|
return getNumber(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '"').isPresent()) {
|
||||||
|
return getString(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(E0014);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StringLiteral getString(FluentStream ps) {
|
||||||
|
var spanStart = ps.index;
|
||||||
|
|
||||||
|
ps.expectChar('"');
|
||||||
|
var value = new StringBuilder();
|
||||||
|
|
||||||
|
Predicate<Character> filter = x -> x != '"' && x != EOL;
|
||||||
|
Optional<Character> opt;
|
||||||
|
while ((opt = ps.takeChar(filter)).isPresent()) {
|
||||||
|
var ch = opt.get();
|
||||||
|
value.append(ch == '\\' ? getEscapeSequence(ps) : ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == EOL).isPresent()) {
|
||||||
|
throw new ParseException(E0020);
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.expectChar('"');
|
||||||
|
|
||||||
|
var stringLiteral = new StringLiteral(value.toString());
|
||||||
|
if (withSpans) {
|
||||||
|
stringLiteral.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getEscapeSequence(FluentStream ps) {
|
||||||
|
var nextOpt = ps.currentChar();
|
||||||
|
if (nextOpt.isEmpty()) {
|
||||||
|
throw new ParseException(E0025, (Character) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = nextOpt.get();
|
||||||
|
return switch (next) {
|
||||||
|
case '\\', '"' -> {
|
||||||
|
ps.next();
|
||||||
|
yield "\\" + next;
|
||||||
|
}
|
||||||
|
case 'u' -> getUnicodeEscapeSequence(ps, next, 4);
|
||||||
|
case 'U' -> getUnicodeEscapeSequence(ps, next, 6);
|
||||||
|
default -> throw new ParseException(E0025, next);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUnicodeEscapeSequence(FluentStream ps, Character u, int digits) {
|
||||||
|
ps.expectChar(u);
|
||||||
|
|
||||||
|
var sequence = new StringBuilder();
|
||||||
|
for (int i = 0; i < digits; i++) {
|
||||||
|
var opt = ps.takeHexDigit();
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
throw new ParseException(E0026, "\\%s%s%s".formatted(u, sequence, ps.currentChar().orElseThrow()));
|
||||||
|
}
|
||||||
|
sequence.append(opt.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\\%s%s".formatted(u, sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NumberLiteral getNumber(FluentStream ps) {
|
||||||
|
var spanStart = ps.index;
|
||||||
|
var value = new StringBuilder();
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '-').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
value.append("-");
|
||||||
|
}
|
||||||
|
value.append(getDigits(ps));
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == '.').isPresent()) {
|
||||||
|
ps.next();
|
||||||
|
value.append(".").append(getDigits(ps));
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberLiteral = new NumberLiteral(value.toString());
|
||||||
|
if (withSpans) {
|
||||||
|
numberLiteral.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return numberLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDigits(FluentStream ps) {
|
||||||
|
var num = new StringBuilder();
|
||||||
|
|
||||||
|
Optional<Character> opt;
|
||||||
|
while ((opt = ps.takeDigit()).isPresent()) {
|
||||||
|
num.append(opt.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num.isEmpty()) {
|
||||||
|
throw new ParseException(E0004, "0-9");
|
||||||
|
}
|
||||||
|
|
||||||
|
return num.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a token representing an indent. It's not part of the AST and it will
|
||||||
|
// be trimmed and merged into adjacent TextElements, or turned into a new
|
||||||
|
// TextElement, if it's surrounded by two Placeables.
|
||||||
|
private PatternElement getIndent(FluentStream ps, String value, int start) {
|
||||||
|
return new Indent(value, start, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Identifier getIdentifier(FluentStream ps) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
|
||||||
|
var name = new StringBuilder().append(ps.takeIDStart());
|
||||||
|
Optional<Character> opt;
|
||||||
|
while ((opt = ps.takeIDChar()).isPresent()) {
|
||||||
|
name.append(opt.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
var identifier = new Identifier(name.toString());
|
||||||
|
if (withSpans) {
|
||||||
|
identifier.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseComment getComment(FluentStream ps) {
|
||||||
|
int spanStart = ps.index;
|
||||||
|
|
||||||
|
final int ANY = -1;
|
||||||
|
final int COMMENT = 0;
|
||||||
|
final int GROUP_COMMENT = 1;
|
||||||
|
final int RESOURCE_COMMENT = 2;
|
||||||
|
|
||||||
|
var level = ANY;
|
||||||
|
var content = new StringBuilder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var i = -1;
|
||||||
|
|
||||||
|
int thisLevel;
|
||||||
|
if (level == ANY) {
|
||||||
|
thisLevel = RESOURCE_COMMENT;
|
||||||
|
} else {
|
||||||
|
thisLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ps.currentChar().filter(v -> v =='#').isPresent() && i < thisLevel) {
|
||||||
|
ps.next();
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level == ANY) {
|
||||||
|
level = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.currentChar().filter(v -> v == EOL).isEmpty()) {
|
||||||
|
ps.expectChar(' ');
|
||||||
|
Optional<Character> opt;
|
||||||
|
while ((opt = ps.takeChar(v -> v != EOL)).isPresent()) {
|
||||||
|
content.append(opt.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.isNextLineComment(level)) {
|
||||||
|
ps.currentChar().ifPresent(content::append);
|
||||||
|
ps.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseComment comment = switch (level) {
|
||||||
|
case COMMENT -> new Comment(content.toString());
|
||||||
|
case GROUP_COMMENT -> new GroupComment(content.toString());
|
||||||
|
default -> new ResourceComment(content.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (withSpans) {
|
||||||
|
comment.addSpan(spanStart, ps.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import ru.di9.fluent.syntax.MathUtils;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static ru.di9.fluent.syntax.StringUtils.*;
|
||||||
|
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.E0003;
|
||||||
|
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.E0004;
|
||||||
|
|
||||||
|
public class FluentStream extends ParserStream {
|
||||||
|
public static final char EOL = '\n';
|
||||||
|
public static final String SPECIAL_LINE_START_CHARS = "}.[*";
|
||||||
|
public static final Pattern VALID_FUNCTION_NAME = Pattern.compile("^[A-Z][A-Z0-9_-]*$");
|
||||||
|
public static final Pattern TRAILING_WS_RE = Pattern.compile("[ \t\n\r]+$");
|
||||||
|
|
||||||
|
public FluentStream(String string) {
|
||||||
|
super(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String peekBlankInline() {
|
||||||
|
int start = index + peekOffset;
|
||||||
|
while (currentPeek().filter(v -> v == ' ').isPresent()) {
|
||||||
|
peek();
|
||||||
|
}
|
||||||
|
return string.substring(start, (index + peekOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String skipBlankInline() {
|
||||||
|
var blank = peekBlankInline();
|
||||||
|
skipToPeek();
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String peekBlankBlock() {
|
||||||
|
var blank = new StringBuilder();
|
||||||
|
while (true) {
|
||||||
|
var lineStart = peekOffset;
|
||||||
|
peekBlankInline();
|
||||||
|
var currentPeek = currentPeek();
|
||||||
|
if (currentPeek.filter(v -> v == EOL).isPresent()) {
|
||||||
|
blank.append(EOL);
|
||||||
|
peek();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentPeek.isEmpty()/*EOF*/) {
|
||||||
|
// Treat the blank line at EOF as a blank block.
|
||||||
|
return blank.toString();
|
||||||
|
}
|
||||||
|
// Any other char; reset to column 1 on this line.
|
||||||
|
resetPeek(lineStart);
|
||||||
|
return blank.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String skipBlankBlock() {
|
||||||
|
var blank = peekBlankBlock();
|
||||||
|
skipToPeek();
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void peekBlank() {
|
||||||
|
while (currentPeek().filter(v -> v == ' ' || v == EOL).isPresent()) {
|
||||||
|
peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skipBlank() {
|
||||||
|
peekBlank();
|
||||||
|
skipToPeek();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expectChar(char ch) {
|
||||||
|
currentChar().filter(v -> v == ch)
|
||||||
|
.orElseThrow(() -> new ParseException(E0003, ch));
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expectLineEnd() {
|
||||||
|
var opt = currentChar();
|
||||||
|
|
||||||
|
if (opt.isEmpty()/*EOF*/) {
|
||||||
|
// EOF is a valid line end in Fluent.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.get() == EOL) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicode Character 'SYMBOL FOR NEWLINE' (U+2424)
|
||||||
|
//noinspection UnnecessaryUnicodeEscape
|
||||||
|
throw new ParseException(E0003, "\u2424");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> takeChar(Predicate<Character> f) {
|
||||||
|
var ch = currentChar().filter(f);
|
||||||
|
ch.ifPresent(x -> next());
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIdentifierStart() {
|
||||||
|
return currentPeek().map(this::isCharIdStart).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNumberStart() {
|
||||||
|
var opt = currentChar();
|
||||||
|
if (opt.filter(v -> v == '-').isPresent()) {
|
||||||
|
opt = peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean result = opt.map(ch -> inRange_09((int) ch)).orElse(false);
|
||||||
|
resetPeek();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValueStart() {
|
||||||
|
// Inline Patterns may start with any char.
|
||||||
|
return currentPeek().filter(v -> v != EOL).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValueContinuation() {
|
||||||
|
var column1 = peekOffset;
|
||||||
|
peekBlankInline();
|
||||||
|
|
||||||
|
if (currentPeek().filter(v -> v == '{').isPresent()) {
|
||||||
|
resetPeek(column1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peekOffset - column1 == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPeek().map(this::isCharPatternContinuation).orElse(false)) {
|
||||||
|
resetPeek(column1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param level <ul>
|
||||||
|
* <li>-1 - any
|
||||||
|
* <li>0 - comment
|
||||||
|
* <li>1 - group comment
|
||||||
|
* <li>2 - resource comment
|
||||||
|
*/
|
||||||
|
public boolean isNextLineComment(int level) {
|
||||||
|
if (currentChar().filter(v -> v != EOL).isPresent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lvl = MathUtils.clamp(level, -1, 2);
|
||||||
|
var i = 0;
|
||||||
|
while (i <= lvl || (lvl == -1 && i < 3)) {
|
||||||
|
if (peek().filter(v -> v != '#').isPresent()) {
|
||||||
|
if (i <= lvl && lvl != -1) {
|
||||||
|
resetPeek();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first char after #, ## or ###.
|
||||||
|
boolean result = peek().filter(ch -> ch == ' ' || ch == EOL).isPresent();
|
||||||
|
resetPeek();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVariantStart() {
|
||||||
|
var currentPeekOffset = peekOffset;
|
||||||
|
if (currentPeek().filter(v -> v == '*').isPresent()) {
|
||||||
|
peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean result = currentPeek().filter(v -> v == '[').isPresent();
|
||||||
|
resetPeek(currentPeekOffset);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAttributeStart() {
|
||||||
|
return currentPeek().filter(v -> v == '.').isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skipToNextEntryStart(int junkStart) {
|
||||||
|
var lastNewline = string.lastIndexOf(EOL, index);
|
||||||
|
if (junkStart < lastNewline) {
|
||||||
|
// Last seen newline is _after_ the junk start. It's safe to rewind
|
||||||
|
// without the risk of resuming at the same broken entry.
|
||||||
|
index = lastNewline;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (currentChar().isPresent()) {
|
||||||
|
// We're only interested in beginnings of line.
|
||||||
|
if (currentChar().filter(v -> v != EOL).isPresent()) {
|
||||||
|
next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break if the first char in this line looks like an entry start.
|
||||||
|
var firstOpt = next();
|
||||||
|
if (firstOpt.isEmpty()/*EOF*/) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char first = firstOpt.get();
|
||||||
|
if (isCharIdStart(first) || first == '-' || first == '#') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public char takeIDStart() {
|
||||||
|
if (currentChar().map(this::isCharIdStart).orElse(false)) {
|
||||||
|
var ret = currentChar();
|
||||||
|
if (ret.isPresent()) {
|
||||||
|
next();
|
||||||
|
return ret.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(E0004, "a-zA-Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> takeIDChar() {
|
||||||
|
return takeChar(ch -> {
|
||||||
|
int cc = ch;
|
||||||
|
return inRange_az(cc) || inRange_AZ(cc) || inRange_09(cc) || cc == 95 /*_*/ || cc == 45 /*-*/;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> takeDigit() {
|
||||||
|
return takeChar(ch -> inRange_09((int) ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> takeHexDigit() {
|
||||||
|
return takeChar(ch -> {
|
||||||
|
int cc = ch;
|
||||||
|
return inRange_09(cc) || inRange_AF(cc) || inRange_af(cc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private boolean isCharIdStart(char ch) {
|
||||||
|
return inRange_az(ch) || inRange_AZ(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCharPatternContinuation(char ch) {
|
||||||
|
return SPECIAL_LINE_START_CHARS.indexOf(ch) < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import ru.di9.fluent.syntax.ast.PatternElement;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Indent extends PatternElement {
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public Indent(String value, int start, int end) {
|
||||||
|
this(value);
|
||||||
|
this.addSpan(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ParseException extends RuntimeException {
|
||||||
|
private final ErrorCode code;
|
||||||
|
private final Object[] args;
|
||||||
|
|
||||||
|
public ParseException(ErrorCode code, Object... args) {
|
||||||
|
super(code.getMessage().formatted(args));
|
||||||
|
this.code = code;
|
||||||
|
this.args = args;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum ErrorCode {
|
||||||
|
E0001("Generic Error"),
|
||||||
|
E0002("Expected an entry start"),
|
||||||
|
E0003("Expected token: \"%s\""),
|
||||||
|
E0004("Expected a character from range: \"%s\""),
|
||||||
|
E0005("Expected message \"%s\" to have a value or attributes"),
|
||||||
|
E0006("Expected term \"-%s\" to have a value"),
|
||||||
|
E0007("Keyword cannot end with a whitespace"),
|
||||||
|
E0008("The callee has to be an upper-case identifier or a term"),
|
||||||
|
E0009("The argument name has to be a simple identifier"),
|
||||||
|
E0010("Expected one of the variants to be marked as default (*)"),
|
||||||
|
E0011("Expected at least one variant after \"->\""),
|
||||||
|
E0012("Expected value"),
|
||||||
|
E0013("Expected variant key"),
|
||||||
|
E0014("Expected literal"),
|
||||||
|
E0015("Only one variant can be marked as default (*)"),
|
||||||
|
E0016("Message references cannot be used as selectors"),
|
||||||
|
E0017("Terms cannot be used as selectors"),
|
||||||
|
E0018("Attributes of messages cannot be used as selectors"),
|
||||||
|
E0019("Attributes of terms cannot be used as placeables"),
|
||||||
|
E0020("Unterminated string expression"),
|
||||||
|
E0021("Positional arguments must not follow named arguments"),
|
||||||
|
E0022("Named arguments must be unique"),
|
||||||
|
E0024("Cannot access variants of a message."),
|
||||||
|
E0025("Unknown escape sequence: \\%s."),
|
||||||
|
E0026("Invalid Unicode escape sequence: %s."),
|
||||||
|
E0027("Unbalanced closing brace in TextElement."),
|
||||||
|
E0028("Expected an inline expression");
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static ru.di9.fluent.syntax.StringUtils.getCharAt;
|
||||||
|
|
||||||
|
public class ParserStream {
|
||||||
|
|
||||||
|
protected int index = 0;
|
||||||
|
protected int peekOffset = 0;
|
||||||
|
|
||||||
|
protected String string;
|
||||||
|
|
||||||
|
public ParserStream(String string) {
|
||||||
|
this.string = string;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> currentChar() {
|
||||||
|
return charAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> currentPeek() {
|
||||||
|
return charAt(index + peekOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> next() {
|
||||||
|
peekOffset = 0;
|
||||||
|
if (index >= string.length()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip over the CRLF as if it was a single character.
|
||||||
|
getCharAt(string, index)
|
||||||
|
.filter(v -> v == '\r')
|
||||||
|
.flatMap(x -> getCharAt(string, index + 1).filter(v -> v == '\n'))
|
||||||
|
.ifPresent(x -> index++);
|
||||||
|
|
||||||
|
index++;
|
||||||
|
return getCharAt(string, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Character> peek() {
|
||||||
|
// Skip over the CRLF as if it was a single character.
|
||||||
|
getCharAt(string, index + peekOffset)
|
||||||
|
.filter(v -> v == '\r')
|
||||||
|
.flatMap(x -> getCharAt(string, index + peekOffset + 1).filter(v -> v == '\n'))
|
||||||
|
.ifPresent(x -> peekOffset++);
|
||||||
|
|
||||||
|
peekOffset++;
|
||||||
|
return getCharAt(string, index + peekOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPeek() {
|
||||||
|
resetPeek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPeek(int offset) {
|
||||||
|
this.peekOffset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skipToPeek() {
|
||||||
|
this.index += this.peekOffset;
|
||||||
|
this.peekOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private Optional<Character> charAt(int offset) {
|
||||||
|
// When the cursor is at CRLF, return LF but don't move the cursor.
|
||||||
|
// The cursor still points to the EOL position, which in this case is the
|
||||||
|
// beginning of the compound CRLF sequence. This ensures slices of
|
||||||
|
// [inclusive, exclusive) continue to work properly.
|
||||||
|
|
||||||
|
var ch = getCharAt(string, offset);
|
||||||
|
var opt = ch.filter(v -> v == '\r')
|
||||||
|
.flatMap(x -> getCharAt(string, offset + 1).filter(v -> v == '\n'));
|
||||||
|
|
||||||
|
return opt.isPresent() ? opt : ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package ru.di9.fluent.syntax.processor;
|
||||||
|
|
||||||
|
import ru.di9.fluent.syntax.ast.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.MatchResult;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
|
public class Processor {
|
||||||
|
private static final java.util.regex.Pattern SPECIAL
|
||||||
|
= java.util.regex.Pattern.compile("\\\\(([\\\\\"])|(u[0-9a-fA-F]{4})|(U[0-90a-fA-F]{6}))");
|
||||||
|
|
||||||
|
public Pattern unescapeLiteralsToText(Pattern pattern) {
|
||||||
|
var result = new Pattern();
|
||||||
|
textFromLiterals(pattern, result.getElements());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pattern escapeTextToLiterals(Pattern pattern) {
|
||||||
|
var result = new Pattern();
|
||||||
|
literalsFromText(pattern, result.getElements());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void textFromLiterals(Pattern pattern, Collection<PatternElement> collection) {
|
||||||
|
TextElement lastText = null;
|
||||||
|
for (PatternElement element : pattern.getElements()) {
|
||||||
|
if (element instanceof TextElement txtElm) {
|
||||||
|
if (lastText == null) {
|
||||||
|
lastText = new TextElement(txtElm.getValue());
|
||||||
|
} else {
|
||||||
|
lastText.setValue(lastText.getValue() + txtElm.getValue());
|
||||||
|
}
|
||||||
|
} else if (element instanceof Placeable plcElm) {
|
||||||
|
InsidePlaceable expression = plcElm.getExpression();
|
||||||
|
if (expression instanceof StringLiteral strExp) {
|
||||||
|
String content = strExp.getValue();
|
||||||
|
{
|
||||||
|
Matcher matcher = SPECIAL.matcher(content);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
while (matcher.find()) {
|
||||||
|
String replacement = unescape(matcher.toMatchResult()).replace("\\", "\\\\");
|
||||||
|
matcher.appendReplacement(sb, replacement);
|
||||||
|
}
|
||||||
|
matcher.appendTail(sb);
|
||||||
|
content = sb.toString();
|
||||||
|
}
|
||||||
|
if (lastText == null) {
|
||||||
|
lastText = new TextElement("");
|
||||||
|
}
|
||||||
|
lastText.setValue(lastText.getValue() + content);
|
||||||
|
} else if (expression instanceof SelectExpression selExp) {
|
||||||
|
List<Variant> processedVariants = new ArrayList<>();
|
||||||
|
for (Variant variant : selExp.getVariants()) {
|
||||||
|
processedVariants.add(new Variant(variant.getKey(), unescapeLiteralsToText(variant.getValue()), variant.isDefault()));
|
||||||
|
}
|
||||||
|
var processedSelect = new SelectExpression(selExp.getSelector(), processedVariants);
|
||||||
|
var placeable = new Placeable(processedSelect);
|
||||||
|
|
||||||
|
if (lastText != null) {
|
||||||
|
collection.add(lastText);
|
||||||
|
lastText = null;
|
||||||
|
}
|
||||||
|
collection.add(placeable);
|
||||||
|
} else {
|
||||||
|
if (lastText != null) {
|
||||||
|
collection.add(lastText);
|
||||||
|
lastText = null;
|
||||||
|
}
|
||||||
|
collection.add(plcElm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastText != null) {
|
||||||
|
collection.add(lastText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void literalsFromText(Pattern pattern, Collection<PatternElement> collection) {
|
||||||
|
for (PatternElement element : pattern.getElements()) {
|
||||||
|
if (element instanceof TextElement txtElm) {
|
||||||
|
if (txtElm.getValue().startsWith(" ") || txtElm.getValue().startsWith("\n")) {
|
||||||
|
collection.add(new Placeable(new StringLiteral("")));
|
||||||
|
}
|
||||||
|
|
||||||
|
int startIndex = 0;
|
||||||
|
for (int i = 0; i < txtElm.getValue().length(); i++) {
|
||||||
|
char ch = txtElm.getValue().charAt(i);
|
||||||
|
if (ch == '{' || ch == '}') {
|
||||||
|
String before = txtElm.getValue().substring(startIndex, i);
|
||||||
|
if (!before.isEmpty()) {
|
||||||
|
collection.add(new TextElement(before));
|
||||||
|
}
|
||||||
|
collection.add(new Placeable(new StringLiteral(String.valueOf(ch))));
|
||||||
|
startIndex = i + 1;
|
||||||
|
} else if (ch == '[' || ch == '*' || ch == '.') {
|
||||||
|
if (i > 0 && txtElm.getValue().charAt(i - 1) == '\n') {
|
||||||
|
String before = txtElm.getValue().substring(startIndex, i);
|
||||||
|
collection.add(new TextElement(before));
|
||||||
|
collection.add(new Placeable(new StringLiteral(String.valueOf(ch))));
|
||||||
|
startIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((txtElm.getValue().length() - 1) > startIndex) {
|
||||||
|
collection.add(new TextElement(txtElm.getValue().substring(startIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txtElm.getValue().endsWith(" ") || txtElm.getValue().endsWith("\n")) {
|
||||||
|
collection.add(new Placeable(new StringLiteral("")));
|
||||||
|
}
|
||||||
|
} else if (element instanceof Placeable plcElm) {
|
||||||
|
if (plcElm.getExpression() instanceof SelectExpression selExp) {
|
||||||
|
List<Variant> rawVariants = new ArrayList<>();
|
||||||
|
for (Variant variant : selExp.getVariants()) {
|
||||||
|
rawVariants.add(new Variant(variant.getKey(), escapeTextToLiterals(variant.getValue()), variant.isDefault()));
|
||||||
|
}
|
||||||
|
collection.add(new Placeable(new SelectExpression(selExp.getSelector(), rawVariants)));
|
||||||
|
} else {
|
||||||
|
collection.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String unescape(MatchResult matchResult) {
|
||||||
|
List<String> groupValues = new ArrayList<>();
|
||||||
|
for (int i = 2; i < matchResult.groupCount() + 1; i++) {
|
||||||
|
groupValues.add(matchResult.group(i) != null ? matchResult.group(i) : "");
|
||||||
|
}
|
||||||
|
Iterator<String> matches = groupValues.iterator();
|
||||||
|
String simple = matches.next();
|
||||||
|
if (!simple.isEmpty()) {
|
||||||
|
return simple;
|
||||||
|
}
|
||||||
|
|
||||||
|
String uni4 = matches.next();
|
||||||
|
if (!uni4.isEmpty()) {
|
||||||
|
int codepoint = Integer.valueOf(uni4.substring(1), 16);
|
||||||
|
if (Character.isBmpCodePoint(codepoint)) {
|
||||||
|
char chr = (char) codepoint;
|
||||||
|
if (!Character.isSurrogate(chr)) {
|
||||||
|
return Character.toString(chr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String uni6 = matches.next();
|
||||||
|
if (!uni6.isEmpty()) {
|
||||||
|
int codepoint = Integer.valueOf(uni6.substring(1), 16);
|
||||||
|
if (Character.isValidCodePoint(codepoint)) {
|
||||||
|
char chr = (char) codepoint;
|
||||||
|
if (!Character.isSurrogate(chr)) {
|
||||||
|
return String.valueOf(Character.highSurrogate(codepoint)) + Character.lowSurrogate(codepoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\uFFFD"; // <20>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package ru.di9.fluent.syntax.serializer;
|
||||||
|
|
||||||
|
import ru.di9.fluent.syntax.ast.*;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class FluentSerializer {
|
||||||
|
public boolean withJunk = false;
|
||||||
|
|
||||||
|
public String serialize(Resource resource) {
|
||||||
|
return resource.getBody()
|
||||||
|
.stream()
|
||||||
|
.map(this::serialize)
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String serialize(TopLevel topLevel) {
|
||||||
|
if (topLevel instanceof Entry entry) {
|
||||||
|
return serializeEntry(entry);
|
||||||
|
} else if (topLevel instanceof Whitespace whitespace) {
|
||||||
|
return whitespace.getContent();
|
||||||
|
} else if (topLevel instanceof Junk junk) {
|
||||||
|
return withJunk ? junk.getContent() : "";
|
||||||
|
} else {
|
||||||
|
throw new SerializeException("Unknown top-level entry type '%s'".formatted(topLevel.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private String serializeEntry(Entry entry) {
|
||||||
|
if (entry instanceof Message message) {
|
||||||
|
return serializeMessage(message);
|
||||||
|
} else if (entry instanceof Term term) {
|
||||||
|
return serializeTerm(term);
|
||||||
|
} else if (entry instanceof Comment comment) {
|
||||||
|
return serializeComment(comment, "#");
|
||||||
|
} else if (entry instanceof GroupComment comment) {
|
||||||
|
return serializeComment(comment, "##");
|
||||||
|
} else if (entry instanceof ResourceComment comment) {
|
||||||
|
return serializeComment(comment, "###");
|
||||||
|
} else {
|
||||||
|
throw new SerializeException("Unknown entry type '%s'".formatted(entry.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeMessage(Message message) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
message.getComment().ifPresent(comment ->
|
||||||
|
builder.append(serializeComment(comment, "#")));
|
||||||
|
|
||||||
|
builder.append(message.getId().getName()).append(" =");
|
||||||
|
|
||||||
|
message.getValue().ifPresent(value ->
|
||||||
|
builder.append(serializePattern(value)));
|
||||||
|
|
||||||
|
for (Attribute attribute : message.getAttributes()) {
|
||||||
|
builder.append(serializeAttribute(attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.append('\n').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeTerm(Term term) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
term.getComment().ifPresent(comment ->
|
||||||
|
builder.append(serializeComment(comment, "#")));
|
||||||
|
|
||||||
|
builder
|
||||||
|
.append('-').append(term.getId().getName()).append(" =")
|
||||||
|
.append(serializePattern(term.getValue()));
|
||||||
|
|
||||||
|
for (Attribute attribute : term.getAttributes()) {
|
||||||
|
builder.append(serializeAttribute(attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.append('\n').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeComment(BaseComment comment, String prefix) {
|
||||||
|
return Arrays.stream(comment.getContent().split("\n"))
|
||||||
|
.map(line -> {
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
return prefix + "\n";
|
||||||
|
} else {
|
||||||
|
return prefix + " " + line + "\n";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializePattern(Pattern pattern) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
String content = pattern.getElements()
|
||||||
|
.stream()
|
||||||
|
.map(this::serializeElement)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(string -> string.replaceAll("\n", "\n "))
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
|
||||||
|
if (shouldStartOnNewLine(pattern)) {
|
||||||
|
builder.append("\n ");
|
||||||
|
} else {
|
||||||
|
builder.append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.append(content).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeAttribute(Attribute attribute) {
|
||||||
|
String value = serializePattern(attribute.getValue()).replaceAll("\n", "\n ");
|
||||||
|
return "\n ." + attribute.getId().getName() + " =" + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeElement(PatternElement patternElement) {
|
||||||
|
if (patternElement instanceof TextElement textElement) {
|
||||||
|
return textElement.getValue();
|
||||||
|
} else if (patternElement instanceof Placeable placeable) {
|
||||||
|
return serializePlaceable(placeable);
|
||||||
|
} else {
|
||||||
|
throw new SerializeException("Unknown element type: '%s".formatted(patternElement.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializePlaceable(Placeable placeable) {
|
||||||
|
InsidePlaceable expression = placeable.getExpression();
|
||||||
|
if (expression instanceof Placeable placeable1) {
|
||||||
|
return "{" + serializePlaceable(placeable1) + "}";
|
||||||
|
} else if (expression instanceof SelectExpression selectExpression) {
|
||||||
|
return "{ " + serializeExpression(selectExpression) + "}";
|
||||||
|
} else if (expression instanceof Expression expression1) {
|
||||||
|
return "{ " + serializeExpression(expression1) + " }";
|
||||||
|
} else {
|
||||||
|
throw new SerializeException("Unknown placeable type '%s'".formatted(expression.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeExpression(Expression expression) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
if (expression instanceof StringLiteral stringLiteral) {
|
||||||
|
builder.append('"').append(stringLiteral.getValue()).append('"');
|
||||||
|
} else if (expression instanceof NumberLiteral numberLiteral) {
|
||||||
|
builder.append(numberLiteral.getValue());
|
||||||
|
} else if (expression instanceof VariableReference variableReference) {
|
||||||
|
builder.append('$').append(variableReference.getId().getName());
|
||||||
|
} else if (expression instanceof TermReference termReference) {
|
||||||
|
builder.append('-').append(termReference.getId().getName());
|
||||||
|
|
||||||
|
termReference.getAttribute().ifPresent(attribute ->
|
||||||
|
builder.append('.').append(attribute.getName()));
|
||||||
|
|
||||||
|
termReference.getArguments().ifPresent(arguments ->
|
||||||
|
builder.append(serializeCallArguments(arguments)));
|
||||||
|
} else if (expression instanceof MessageReference messageReference) {
|
||||||
|
builder.append(messageReference.getId().getName());
|
||||||
|
|
||||||
|
messageReference.getAttribute().ifPresent(attribute ->
|
||||||
|
builder.append('.').append(attribute.getName()));
|
||||||
|
} else if (expression instanceof FunctionReference functionReference) {
|
||||||
|
builder
|
||||||
|
.append(functionReference.getId().getName())
|
||||||
|
.append(serializeCallArguments(functionReference.getArguments()));
|
||||||
|
} else if (expression instanceof SelectExpression selectExpression) {
|
||||||
|
builder.append(serializeExpression(selectExpression.getSelector())).append(" ->");
|
||||||
|
for (Variant variant : selectExpression.getVariants()) {
|
||||||
|
builder.append(serializeVariant(variant));
|
||||||
|
}
|
||||||
|
builder.append('\n');
|
||||||
|
} else {
|
||||||
|
throw new SerializeException("Unknown expression type '%s".formatted(expression.getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeCallArguments(CallArguments callArguments) {
|
||||||
|
boolean hasPositional = !callArguments.getPositional().isEmpty();
|
||||||
|
boolean hasNamed = !callArguments.getNamed().isEmpty();
|
||||||
|
var builder = new StringBuilder("(");
|
||||||
|
|
||||||
|
if (hasPositional) {
|
||||||
|
var positional = callArguments.getPositional()
|
||||||
|
.stream()
|
||||||
|
.map(this::serializeExpression)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
builder.append(positional);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNamed) {
|
||||||
|
var named = callArguments.getNamed()
|
||||||
|
.stream()
|
||||||
|
.map(this::serializeNamedArgument)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
if (hasPositional) {
|
||||||
|
builder.append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(named);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.append(')').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeVariant(Variant variant) {
|
||||||
|
var key = serializeVariantKey(variant.getKey());
|
||||||
|
var value = serializePattern(variant.getValue()).replaceAll("\n", "\n ");
|
||||||
|
var builder = new StringBuilder("\n ");
|
||||||
|
|
||||||
|
if (variant.isDefault()) {
|
||||||
|
builder.append('*');
|
||||||
|
} else {
|
||||||
|
builder.append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.append('[').append(key).append(']').append(value).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeNamedArgument(NamedArgument namedArgument) {
|
||||||
|
return namedArgument.getName().getName() + ": " + serializeExpression(namedArgument.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeVariantKey(VariantKey variantKey) {
|
||||||
|
if (variantKey instanceof Identifier identifier) {
|
||||||
|
return identifier.getName();
|
||||||
|
} else if (variantKey instanceof NumberLiteral numberLiteral) {
|
||||||
|
return numberLiteral.getValue();
|
||||||
|
} else {
|
||||||
|
throw new SerializeException("Unknown variant key type '%s'".formatted(variantKey.getClass()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldStartOnNewLine(Pattern pattern) {
|
||||||
|
boolean isMultiline = pattern.getElements().stream().anyMatch(it -> isSelectExpr(it) || includesLine(it));
|
||||||
|
if (isMultiline) {
|
||||||
|
if (!pattern.getElements().isEmpty()) {
|
||||||
|
var firstElement = pattern.getElements().get(0);
|
||||||
|
if (firstElement instanceof TextElement te) {
|
||||||
|
if (!te.getValue().isEmpty()) {
|
||||||
|
char firstChar = te.getValue().charAt(0);
|
||||||
|
// Due to the indentation requirement the following characters may not appear
|
||||||
|
// as the first character on a new line.
|
||||||
|
return firstChar != '[' && firstChar != '.' && firstChar != '*';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSelectExpr(PatternElement patternElement) {
|
||||||
|
return patternElement instanceof Placeable pl
|
||||||
|
&& pl.getExpression() instanceof SelectExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean includesLine(PatternElement patternElement) {
|
||||||
|
return patternElement instanceof TextElement te
|
||||||
|
&& te.getValue().contains("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.di9.fluent.syntax.serializer;
|
||||||
|
|
||||||
|
public class SerializeException extends RuntimeException {
|
||||||
|
public SerializeException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package ru.di9.fluent.syntax.visitor;
|
||||||
|
|
||||||
|
import ru.di9.fluent.syntax.ast.BaseNode;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class Visitor {
|
||||||
|
private final Map<String, Method> handlers = new HashMap<>();
|
||||||
|
|
||||||
|
public Visitor() {
|
||||||
|
Method[] methods = this.getClass().getDeclaredMethods();
|
||||||
|
for (Method method : methods) {
|
||||||
|
if (method.getName().startsWith("visit")) {
|
||||||
|
handlers.put(method.getName().substring("visit".length()), method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visit(BaseNode node) {
|
||||||
|
String cName = node.getClass().getSimpleName();
|
||||||
|
Method handler = handlers.get(cName);
|
||||||
|
if (handler != null) {
|
||||||
|
try {
|
||||||
|
handler.invoke(this, node);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
genericVisit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void genericVisit(BaseNode node) {
|
||||||
|
Iterator<Pair> itr = childrenOf(node);
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
Pair pair = itr.next();
|
||||||
|
Object value = pair.invokeResult();
|
||||||
|
|
||||||
|
if (value instanceof Optional<?> opt) {
|
||||||
|
value = opt.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof BaseNode baseNode) {
|
||||||
|
this.visit(baseNode);
|
||||||
|
} else if (value instanceof Collection<?> collection) {
|
||||||
|
for (Object it : collection) {
|
||||||
|
if (it instanceof BaseNode baseNode) {
|
||||||
|
this.visit(baseNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Pair(String name, Object invokeResult){}
|
||||||
|
|
||||||
|
static Iterator<Pair> childrenOf(BaseNode node) {
|
||||||
|
final Method[] methods = node.getClass().getMethods();
|
||||||
|
Arrays.sort(methods, Comparator.comparing(Method::getName));
|
||||||
|
|
||||||
|
return new Iterator<>() {
|
||||||
|
private int idx = 0;
|
||||||
|
private Method nextMethod;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
if (nextMethod != null) return true;
|
||||||
|
else if (idx >= methods.length) return false;
|
||||||
|
return (nextMethod = loadNext()) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pair next() {
|
||||||
|
if (idx >= methods.length) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
} else if (nextMethod == null) {
|
||||||
|
if ((nextMethod = loadNext()) == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object invokeResult;
|
||||||
|
try {
|
||||||
|
invokeResult = nextMethod.invoke(node);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pair = new Pair(nextMethod.getName(), invokeResult);
|
||||||
|
nextMethod = null;
|
||||||
|
return pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method loadNext() {
|
||||||
|
do {
|
||||||
|
Method method = methods[idx++];
|
||||||
|
if (Modifier.isPublic(method.getModifiers())
|
||||||
|
&& method.getName().startsWith("get")
|
||||||
|
&& method.getParameterCount() == 0
|
||||||
|
&& !method.getName().equals("getClass")) {
|
||||||
|
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
} while (idx < methods.length);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ru.di9.fluent.syntax;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class MathUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clamp() {
|
||||||
|
assertThat(MathUtils.clamp(7, 1, 10)).isEqualTo(7);
|
||||||
|
assertThat(MathUtils.clamp(0, 1, 10)).isEqualTo(1);
|
||||||
|
assertThat(MathUtils.clamp(11, 1, 10)).isEqualTo(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package ru.di9.fluent.syntax;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
class StringUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetChar() {
|
||||||
|
String string = "abcd";
|
||||||
|
Optional<Character> chr = StringUtils.getCharAt(string, 0);
|
||||||
|
|
||||||
|
Assertions.assertThat(chr)
|
||||||
|
.hasValue('a');
|
||||||
|
|
||||||
|
chr = StringUtils.getCharAt(string, 4);
|
||||||
|
|
||||||
|
Assertions.assertThat(chr)
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
chr = StringUtils.getCharAt(null, 0);
|
||||||
|
|
||||||
|
Assertions.assertThat(chr)
|
||||||
|
.isEmpty();
|
||||||
|
|
||||||
|
chr = StringUtils.getCharAt("", 0);
|
||||||
|
|
||||||
|
Assertions.assertThat(chr)
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ru.di9.fluent.syntax.ast;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
|
||||||
|
class BaseNodeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEquals() {
|
||||||
|
var m1 = new Message(new Identifier("test-id"), new Pattern(new TextElement("localized")));
|
||||||
|
var m11 = new Message(new Identifier("test-id"), new Pattern(new TextElement("localized")));
|
||||||
|
var m2 = new Message(new Identifier("test-id"), new Pattern(new TextElement("different")));
|
||||||
|
|
||||||
|
assertEquals(m1, m11);
|
||||||
|
assertNotEquals(m1, m2);
|
||||||
|
assertEquals(m1.getId(), m2.getId());
|
||||||
|
assertNotEquals(m1.getValue(), m2.getValue());
|
||||||
|
//Шта?
|
||||||
|
assertNotEquals(m1, null);
|
||||||
|
assertNotEquals(null, m1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
|
import org.junit.jupiter.api.TestFactory;
|
||||||
|
import ru.di9.fluent.test.utils.AstAssert;
|
||||||
|
import ru.di9.fluent.test.utils.Tuple3;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static ru.di9.fluent.test.utils.FileUtils.getExt;
|
||||||
|
import static ru.di9.fluent.test.utils.FileUtils.getName;
|
||||||
|
|
||||||
|
public abstract class AbstractFixturesTest {
|
||||||
|
|
||||||
|
@TestFactory
|
||||||
|
public Iterable<DynamicTest> fixturesTest() throws IOException {
|
||||||
|
List<DynamicTest> tests = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Stream<Path> walker = Files.walk(getFixturesPath())) {
|
||||||
|
walker
|
||||||
|
.filter(path -> path.toFile().isFile())
|
||||||
|
.filter(path -> getExt(path.toFile().getName()).equals("ftl"))
|
||||||
|
.map(path -> {
|
||||||
|
Path parent = path.getParent();
|
||||||
|
String name = getName(path.getFileName().toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Tuple3(
|
||||||
|
name,
|
||||||
|
Files.readString(parent.resolve(name + ".ftl")),
|
||||||
|
Files.readString(parent.resolve(name + ".json"))
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(tuple -> DynamicTest.dynamicTest(tuple.value1(), () -> {
|
||||||
|
var parser = new FluentParser();
|
||||||
|
parser.withSpans = isWithSpans(tuple.value1());
|
||||||
|
parser.withJunkAnnotations = isWithJunkAnnotations(tuple.value1());
|
||||||
|
var resource = parser.parse(tuple.value2());
|
||||||
|
|
||||||
|
AstAssert.assertThat(resource)
|
||||||
|
.isEqualAstJson(tuple.value3());
|
||||||
|
}))
|
||||||
|
.forEachOrdered(tests::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Path getFixturesPath();
|
||||||
|
|
||||||
|
protected boolean isWithSpans(String testName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isWithJunkAnnotations(String testName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static ru.di9.fluent.syntax.parser.ParseException.ErrorCode.E0003;
|
||||||
|
|
||||||
|
class FluentStreamTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPeekBlankInline() {
|
||||||
|
var ps = new FluentStream(" ");
|
||||||
|
assertThat(ps.peekBlankInline()).isEqualTo(" ");
|
||||||
|
|
||||||
|
ps = new FluentStream(" | ");
|
||||||
|
assertThat(ps.peekBlankInline()).isEqualTo(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPeekBlankBlock() {
|
||||||
|
var ps = new FluentStream(" \n \n ");
|
||||||
|
assertThat(ps.peekBlankBlock()).isEqualTo("\n\n");
|
||||||
|
|
||||||
|
ps = new FluentStream(" \n . ");
|
||||||
|
assertThat(ps.peekBlankBlock()).isEqualTo("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSkipBlankBlock() {
|
||||||
|
var ps = new FluentStream(" \n \n ");
|
||||||
|
assertThat(ps.skipBlankBlock()).isEqualTo("\n\n");
|
||||||
|
assertThat(ps.index).isEqualTo(12);
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExpectLineEnd() {
|
||||||
|
var ps1 = new FluentStream("");
|
||||||
|
assertThatNoException().isThrownBy(ps1::expectLineEnd);
|
||||||
|
|
||||||
|
var ps2 = new FluentStream("\n");
|
||||||
|
assertThatNoException().isThrownBy(ps2::expectLineEnd);
|
||||||
|
|
||||||
|
var ps3 = new FluentStream(" ");
|
||||||
|
var catchException = catchThrowableOfType(ps3::expectLineEnd, ParseException.class);
|
||||||
|
assertThat(catchException.getCode()).isEqualTo(E0003);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExpectChar() {
|
||||||
|
var ps = new FluentStream("z");
|
||||||
|
assertThatNoException().isThrownBy(() -> ps.expectChar('z'));
|
||||||
|
var catchException = catchThrowableOfType(() -> ps.expectChar('a'), ParseException.class);
|
||||||
|
assertThat(catchException.getCode()).isEqualTo(E0003);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTakeChar() {
|
||||||
|
var ps = new FluentStream("abc");
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('a');
|
||||||
|
assertThat(ps.takeChar(c -> c == 'a')).hasValue('a');
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
assertThat(ps.takeChar(c -> c == 'c')).isEmpty();
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsNextLineComment() {
|
||||||
|
var ps = new FluentStream("# 123");
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
ps.next();
|
||||||
|
}
|
||||||
|
assertThat(ps.isNextLineComment(0)).isFalse();
|
||||||
|
|
||||||
|
var ps1 = new FluentStream("# 123\n# 456");
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
ps1.next();
|
||||||
|
}
|
||||||
|
assertThat(ps1.isNextLineComment(0)).isTrue();
|
||||||
|
|
||||||
|
var ps2 = new FluentStream("# 123\nkey = value");
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
ps2.next();
|
||||||
|
}
|
||||||
|
assertThat(ps2.isNextLineComment(0)).isFalse();
|
||||||
|
|
||||||
|
var ps3 = new FluentStream("# 123");
|
||||||
|
assertThat(ps3.isNextLineComment(0)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSkipBlankInline() {
|
||||||
|
var ps = new FluentStream(" \n123");
|
||||||
|
|
||||||
|
assertThat(ps.skipBlankInline()).isEqualTo(" ");
|
||||||
|
assertThat(ps.index).isEqualTo(4);
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.currentChar()).hasValue('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPeekBlank() {
|
||||||
|
var ps = new FluentStream(" 123");
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue(' ');
|
||||||
|
assertThat(ps.currentPeek()).hasValue(' ');
|
||||||
|
|
||||||
|
ps.peekBlank();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue(' ');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('1');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSkipBlank() {
|
||||||
|
var ps = new FluentStream(" 123");
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue(' ');
|
||||||
|
assertThat(ps.currentPeek()).hasValue(' ');
|
||||||
|
|
||||||
|
ps.skipBlank();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('1');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('1');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsNumberStart() {
|
||||||
|
var ps = new FluentStream("123");
|
||||||
|
assertThat(ps.isNumberStart()).isTrue();
|
||||||
|
|
||||||
|
var ps1 = new FluentStream("-123");
|
||||||
|
assertThat(ps1.isNumberStart()).isTrue();
|
||||||
|
|
||||||
|
var ps2 = new FluentStream("a123");
|
||||||
|
assertThat(ps2.isNumberStart()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsIdentifierStart() {
|
||||||
|
var ps = new FluentStream("foo = Bar");
|
||||||
|
assertThat(ps.isIdentifierStart()).isTrue();
|
||||||
|
|
||||||
|
var ps1 = new FluentStream("# foo = Bar");
|
||||||
|
assertThat(ps1.isIdentifierStart()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsValueStart() {
|
||||||
|
var ps = new FluentStream("foo = Bar\n");
|
||||||
|
assertThat(ps.isValueStart()).isTrue();
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
ps.next();
|
||||||
|
}
|
||||||
|
assertThat(ps.isValueStart()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ParserStreamTest {
|
||||||
|
ParserStream ps;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ps = new ParserStream("abcd");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void next() {
|
||||||
|
assertThat(ps.currentChar()).hasValue('a');
|
||||||
|
assertThat(ps.index).isEqualTo(0);
|
||||||
|
|
||||||
|
assertThat(ps.next()).hasValue('b');
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ps.next()).hasValue('c');
|
||||||
|
assertThat(ps.currentChar()).hasValue('c');
|
||||||
|
assertThat(ps.index).isEqualTo(2);
|
||||||
|
|
||||||
|
assertThat(ps.next()).hasValue('d');
|
||||||
|
assertThat(ps.currentChar()).hasValue('d');
|
||||||
|
assertThat(ps.index).isEqualTo(3);
|
||||||
|
|
||||||
|
assertThat(ps.next()).isEmpty();
|
||||||
|
assertThat(ps.currentChar()).isEmpty();
|
||||||
|
assertThat(ps.index).isEqualTo(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void peek() {
|
||||||
|
assertThat(ps.currentPeek()).hasValue('a');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('b');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('b');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('c');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('c');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(2);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('d');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('d');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(3);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).isEmpty();
|
||||||
|
assertThat(ps.currentPeek()).isEmpty();
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void peekAndNext() {
|
||||||
|
assertThat(ps.peek()).hasValue('b');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(0);
|
||||||
|
|
||||||
|
assertThat(ps.next()).hasValue('b');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('c');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ps.next()).hasValue('c');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(2);
|
||||||
|
assertThat(ps.currentChar()).hasValue('c');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('c');
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('d');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(2);
|
||||||
|
|
||||||
|
assertThat(ps.next()).hasValue('d');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(3);
|
||||||
|
assertThat(ps.currentChar()).hasValue('d');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('d');
|
||||||
|
|
||||||
|
assertThat(ps.peek()).isEmpty();
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(3);
|
||||||
|
assertThat(ps.currentChar()).hasValue('d');
|
||||||
|
assertThat(ps.currentPeek()).isEmpty();
|
||||||
|
|
||||||
|
assertThat(ps.peek()).isEmpty();
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(2);
|
||||||
|
assertThat(ps.index).isEqualTo(3);
|
||||||
|
|
||||||
|
assertThat(ps.next()).isEmpty();
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void skipAndPeek() {
|
||||||
|
ps.peek();
|
||||||
|
ps.peek();
|
||||||
|
|
||||||
|
ps.skipToPeek();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('c');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('c');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(2);
|
||||||
|
|
||||||
|
ps.peek();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('c');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('d');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(2);
|
||||||
|
|
||||||
|
ps.next();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('d');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('d');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resetPeek() {
|
||||||
|
ps.next();
|
||||||
|
ps.peek();
|
||||||
|
ps.peek();
|
||||||
|
ps.resetPeek();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('b');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
ps.peek();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('c');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
ps.peek();
|
||||||
|
ps.peek();
|
||||||
|
ps.peek();
|
||||||
|
ps.resetPeek();
|
||||||
|
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('b');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(0);
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('c');
|
||||||
|
assertThat(ps.currentChar()).hasValue('b');
|
||||||
|
assertThat(ps.currentPeek()).hasValue('c');
|
||||||
|
assertThat(ps.peekOffset).isEqualTo(1);
|
||||||
|
assertThat(ps.index).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ps.peek()).hasValue('d');
|
||||||
|
assertThat(ps.peek()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexOutOfRange() {
|
||||||
|
ps.next();
|
||||||
|
ps.next();
|
||||||
|
ps.next();
|
||||||
|
ps.next();
|
||||||
|
|
||||||
|
assertThat(ps.index).isGreaterThanOrEqualTo(ps.string.length());
|
||||||
|
assertThat(ps.next()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class ReferenceTest extends AbstractFixturesTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Path getFixturesPath() {
|
||||||
|
return Path.of("src", "test", "resources", "reference_fixtures");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isWithSpans(String testName) {
|
||||||
|
return !testName.equals("crlf") && !testName.equals("select_expressions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isWithJunkAnnotations(String testName) {
|
||||||
|
return !testName.equals("crlf") && !testName.equals("select_expressions");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.di9.fluent.syntax.parser;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class StructureTest extends AbstractFixturesTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Path getFixturesPath() {
|
||||||
|
return Path.of("src", "test", "resources", "structure_fixtures");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package ru.di9.fluent.syntax.processor;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import ru.di9.fluent.syntax.ast.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class ProcessorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toProcessedPattern() {
|
||||||
|
var processor = new Processor();
|
||||||
|
|
||||||
|
var pattern = new Pattern();
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("Hi"));
|
||||||
|
assertEquals(pattern, processor.unescapeLiteralsToText(pattern));
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new Placeable(new StringLiteral("\\\\")),
|
||||||
|
new TextElement(" "),
|
||||||
|
new Placeable(new StringLiteral("\\\""))
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("\\ \"")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Foo "),
|
||||||
|
new Placeable(new StringLiteral("Bar"))
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Foo Bar")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(
|
||||||
|
new TextElement("Foo "),
|
||||||
|
new Placeable(new StringLiteral("Bar"))
|
||||||
|
),
|
||||||
|
pattern
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Hi,"),
|
||||||
|
new Placeable(new StringLiteral("\\u0020")),
|
||||||
|
new TextElement("there")
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Hi, there")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Emoji: "),
|
||||||
|
new Placeable(new StringLiteral("\\U01f602"))
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Emoji: \uD83D\uDE02")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Emoji: 😂")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Illegal escape sequence: "),
|
||||||
|
new Placeable(new StringLiteral("\\ud800"))
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Illegal escape sequence: <20>")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Illegal escape sequence: "),
|
||||||
|
new Placeable(new StringLiteral("\\U00d800"))
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Illegal escape sequence: <20>")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Hi, "),
|
||||||
|
new Placeable(new StringLiteral("{")),
|
||||||
|
new TextElement(" there")
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Hi, { there")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Foo\n"),
|
||||||
|
new Placeable(new StringLiteral(".")),
|
||||||
|
new TextElement("bar")
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(new TextElement("Foo\n.bar")),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().addAll(List.of(
|
||||||
|
new TextElement("Foo "),
|
||||||
|
new Placeable(
|
||||||
|
new SelectExpression(
|
||||||
|
new NumberLiteral("1"),
|
||||||
|
List.of(
|
||||||
|
new Variant(
|
||||||
|
new Identifier("other"),
|
||||||
|
new Pattern(
|
||||||
|
new TextElement("bar "),
|
||||||
|
new Placeable(
|
||||||
|
new StringLiteral("{-_-}")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new TextElement(" baz")
|
||||||
|
));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(
|
||||||
|
new TextElement("Foo "),
|
||||||
|
new Placeable(
|
||||||
|
new SelectExpression(
|
||||||
|
new NumberLiteral("1"),
|
||||||
|
List.of(
|
||||||
|
new Variant(
|
||||||
|
new Identifier("other"),
|
||||||
|
new Pattern(new TextElement("bar {-_-}")),
|
||||||
|
true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new TextElement(" baz")
|
||||||
|
|
||||||
|
),
|
||||||
|
processor.unescapeLiteralsToText(pattern)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toRawPattern() {
|
||||||
|
var processor = new Processor();
|
||||||
|
|
||||||
|
var pattern = new Pattern();
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("Hi"));
|
||||||
|
assertEquals(pattern, processor.escapeTextToLiterals(pattern));
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("\\ \""));
|
||||||
|
assertEquals(pattern, processor.escapeTextToLiterals(pattern));
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("Hi, {-_-}"));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(
|
||||||
|
new TextElement("Hi, "),
|
||||||
|
new Placeable(new StringLiteral("{")),
|
||||||
|
new TextElement("-_-"),
|
||||||
|
new Placeable(new StringLiteral("}"))
|
||||||
|
),
|
||||||
|
processor.escapeTextToLiterals(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("Foo\nbar"));
|
||||||
|
assertEquals(pattern, processor.escapeTextToLiterals(pattern));
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("Foo\n*bar"));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(
|
||||||
|
new TextElement("Foo\n"),
|
||||||
|
new Placeable(new StringLiteral("*")),
|
||||||
|
new TextElement("bar")
|
||||||
|
),
|
||||||
|
processor.escapeTextToLiterals(pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
pattern.getElements().clear();
|
||||||
|
pattern.getElements().add(new TextElement("\nFoo\nbar "));
|
||||||
|
assertEquals(
|
||||||
|
new Pattern(
|
||||||
|
new Placeable(new StringLiteral("")),
|
||||||
|
new TextElement("\nFoo\nbar "),
|
||||||
|
new Placeable(new StringLiteral(""))
|
||||||
|
),
|
||||||
|
processor.escapeTextToLiterals(pattern)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ru.di9.fluent.syntax.serializer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import ru.di9.fluent.syntax.parser.FluentParser;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SerializeEntryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test() {
|
||||||
|
String input = """
|
||||||
|
# Attached comment
|
||||||
|
key = Value""";
|
||||||
|
|
||||||
|
var parser = new FluentParser();
|
||||||
|
var topLevel = parser.parse(input).getBody().get(0);
|
||||||
|
|
||||||
|
var serializer = new FluentSerializer();
|
||||||
|
var serialized = serializer.serialize(topLevel).trim();
|
||||||
|
|
||||||
|
assertThat(serialized).isEqualTo(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package ru.di9.fluent.syntax.serializer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DynamicNode;
|
||||||
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
|
import org.junit.jupiter.api.TestFactory;
|
||||||
|
import ru.di9.fluent.syntax.ast.Resource;
|
||||||
|
import ru.di9.fluent.syntax.parser.FluentParser;
|
||||||
|
import ru.di9.fluent.test.utils.Tuple3;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static ru.di9.fluent.test.utils.FileUtils.getExt;
|
||||||
|
import static ru.di9.fluent.test.utils.FileUtils.getName;
|
||||||
|
|
||||||
|
class SerializeResourceTest {
|
||||||
|
|
||||||
|
@TestFactory
|
||||||
|
Iterable<DynamicTest> test() throws IOException {
|
||||||
|
SortedSet<DynamicTest> tests = new TreeSet<>(Comparator.comparing(DynamicNode::getDisplayName));
|
||||||
|
|
||||||
|
try (Stream<Path> walker = Files.walk(Path.of("src", "test", "resources", "serialized"))) {
|
||||||
|
walker
|
||||||
|
.filter(path -> path.toFile().isFile())
|
||||||
|
.filter(path -> getExt(path).equals("ftl"))
|
||||||
|
.map(path -> {
|
||||||
|
Path parent = path.getParent();
|
||||||
|
String name = getName(path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path ftlSourcePath = parent.resolve(name + ".ftl");
|
||||||
|
Path expSourcePath = parent.resolve(name + ".exp.txt");
|
||||||
|
|
||||||
|
String ftlSource = Files.readString(ftlSourcePath);
|
||||||
|
String expSource;
|
||||||
|
|
||||||
|
if (Files.exists(expSourcePath)) {
|
||||||
|
expSource = Files.readString(expSourcePath);
|
||||||
|
} else {
|
||||||
|
expSource = ftlSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tuple3(name, ftlSource, expSource);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(tuple -> DynamicTest.dynamicTest(tuple.value1(), () -> {
|
||||||
|
FluentParser parser = new FluentParser();
|
||||||
|
Resource resource = parser.parse(tuple.value2());
|
||||||
|
|
||||||
|
FluentSerializer serializer = new FluentSerializer();
|
||||||
|
String serialized = serializer.serialize(resource);
|
||||||
|
|
||||||
|
assertThat(serialized).isEqualTo(tuple.value3());
|
||||||
|
}))
|
||||||
|
.forEach(tests::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package ru.di9.fluent.syntax.visitor;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import ru.di9.fluent.syntax.ast.Identifier;
|
||||||
|
import ru.di9.fluent.syntax.ast.Pattern;
|
||||||
|
import ru.di9.fluent.syntax.ast.TextElement;
|
||||||
|
import ru.di9.fluent.syntax.ast.Variant;
|
||||||
|
import ru.di9.fluent.syntax.parser.FluentParser;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Spliterator;
|
||||||
|
import java.util.Spliterators;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class VisitorTest {
|
||||||
|
FluentParser parser = new FluentParser();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_basics() {
|
||||||
|
var visitor = new TestableVisitor();
|
||||||
|
var source = """
|
||||||
|
msg = foo {$var ->
|
||||||
|
*[other] bar
|
||||||
|
} baz""";
|
||||||
|
var res = parser.parse(source);
|
||||||
|
visitor.visit(res);
|
||||||
|
assertEquals(3, visitor.wordCount);
|
||||||
|
assertEquals(2, visitor.patternCount);
|
||||||
|
assertEquals(1, visitor.variantCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_childrenOf() {
|
||||||
|
var variant = new Variant(new Identifier("other"), new Pattern(), true);
|
||||||
|
Iterator<Visitor.Pair> itr = Visitor.childrenOf(variant);
|
||||||
|
|
||||||
|
Spliterator<Visitor.Pair> spliterator = Spliterators.spliteratorUnknownSize(itr, Spliterator.ORDERED);
|
||||||
|
Stream<Visitor.Pair> stream = StreamSupport.stream(spliterator, false);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
List.of("getKey", "getSpan", "getValue"),
|
||||||
|
stream.map(Visitor.Pair::name).sorted().toList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
static class TestableVisitor extends Visitor {
|
||||||
|
private static final java.util.regex.Pattern WORDS = java.util.regex.Pattern.compile("\\w+");
|
||||||
|
|
||||||
|
private int patternCount = 0;
|
||||||
|
private int variantCount = 0;
|
||||||
|
private int wordCount = 0;
|
||||||
|
|
||||||
|
public void visitPattern(Pattern node) {
|
||||||
|
super.genericVisit(node);
|
||||||
|
patternCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visitVariant(Variant node) {
|
||||||
|
super.genericVisit(node);
|
||||||
|
variantCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visitTextElement(TextElement node) {
|
||||||
|
Matcher matcher = WORDS.matcher(node.getValue());
|
||||||
|
while (matcher.find()) {
|
||||||
|
wordCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ru.di9.fluent.test.utils;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import org.assertj.core.api.AbstractAssert;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||||
|
import ru.di9.fluent.syntax.ast.BaseNode;
|
||||||
|
|
||||||
|
import java.io.PrintStream;
|
||||||
|
|
||||||
|
@SuppressWarnings("UnusedReturnValue")
|
||||||
|
public class AstAssert extends AbstractAssert<AstAssert, BaseNode> {
|
||||||
|
private final Gson gson;
|
||||||
|
private final NullIgnoreComparator nullIgnoreComparator;
|
||||||
|
|
||||||
|
protected AstAssert(BaseNode actualNode) {
|
||||||
|
super(actualNode, AstAssert.class);
|
||||||
|
gson = new GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.registerTypeHierarchyAdapter(BaseNode.class, new BaseNodeJsonSerializer())
|
||||||
|
.create();
|
||||||
|
nullIgnoreComparator = new NullIgnoreComparator(JSONCompareMode.STRICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public AstAssert printAstJson(PrintStream printStream) {
|
||||||
|
var json = gson.toJson(actual);
|
||||||
|
printStream.println(json);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AstAssert isEqualAstJson(String astJson) {
|
||||||
|
var actualJson = gson.toJson(actual);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONAssert.assertEquals(astJson, actualJson, nullIgnoreComparator);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
failWithMessage(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AstAssert assertThat(BaseNode baseNode) {
|
||||||
|
return new AstAssert(baseNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ru.di9.fluent.test.utils;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import org.joor.Reflect;
|
||||||
|
import ru.di9.fluent.syntax.ast.BaseNode;
|
||||||
|
import ru.di9.fluent.syntax.ast.Whitespace;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class BaseNodeJsonSerializer implements JsonSerializer<BaseNode> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(BaseNode baseNode, Type type, JsonSerializationContext ctx) {
|
||||||
|
var root = new JsonObject();
|
||||||
|
root.add("type", new JsonPrimitive(baseNode.getClass().getSimpleName()));
|
||||||
|
|
||||||
|
var fields = Reflect.on(baseNode).fields();
|
||||||
|
|
||||||
|
for (Map.Entry<String, Reflect> field : fields.entrySet()) {
|
||||||
|
String name = field.getKey().equalsIgnoreCase("isDefault") ? "default" : field.getKey();
|
||||||
|
Object value = field.getValue().get();
|
||||||
|
|
||||||
|
if (value instanceof Collection<?> collection) {
|
||||||
|
var jsonArray = new JsonArray();
|
||||||
|
|
||||||
|
for (Object item : collection) {
|
||||||
|
if (item instanceof Whitespace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
jsonArray.add(ctx.serialize(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
root.add(name, jsonArray);
|
||||||
|
} else {
|
||||||
|
root.add(name, ctx.serialize(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.di9.fluent.test.utils;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public interface FileUtils {
|
||||||
|
|
||||||
|
static String getExt(String fileName) {
|
||||||
|
var idx = fileName.lastIndexOf('.');
|
||||||
|
if (idx < 0) return "";
|
||||||
|
return fileName.substring(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getName(String fileName) {
|
||||||
|
var idx = fileName.lastIndexOf('.');
|
||||||
|
if (idx < 0) return fileName;
|
||||||
|
return fileName.substring(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getName(Path pathToFile) {
|
||||||
|
return getName(pathToFile.getFileName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getExt(Path pathToFile) {
|
||||||
|
return getExt(pathToFile.getFileName().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ru.di9.fluent.test.utils;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||||
|
import org.skyscreamer.jsonassert.JSONCompareResult;
|
||||||
|
import org.skyscreamer.jsonassert.comparator.DefaultComparator;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.skyscreamer.jsonassert.comparator.JSONCompareUtil.getKeys;
|
||||||
|
import static org.skyscreamer.jsonassert.comparator.JSONCompareUtil.qualify;
|
||||||
|
|
||||||
|
public class NullIgnoreComparator extends DefaultComparator {
|
||||||
|
public NullIgnoreComparator(JSONCompareMode mode) {
|
||||||
|
super(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void checkJsonObjectKeysExpectedInActual(String prefix, JSONObject expected, JSONObject actual, JSONCompareResult result) throws JSONException {
|
||||||
|
Set<String> expectedKeys = getKeys(expected);
|
||||||
|
for (String key : expectedKeys) {
|
||||||
|
Object expectedValue = expected.get(key);
|
||||||
|
if (actual.has(key)) {
|
||||||
|
Object actualValue = actual.get(key);
|
||||||
|
compareValues(qualify(prefix, key), expectedValue, actualValue, result);
|
||||||
|
} else if (!expectedValue.equals(JSONObject.NULL)) {
|
||||||
|
result.missing(prefix, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.di9.fluent.test.utils;
|
||||||
|
|
||||||
|
public record Tuple3(String value1, String value2, String value3) {
|
||||||
|
}
|
||||||
2
fluent.syntax/src/test/resources/reference_fixtures/.gitattributes
vendored
Normal file
2
fluent.syntax/src/test/resources/reference_fixtures/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
crlf.ftl eol=crlf
|
||||||
|
cr.ftl eol=cr
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# ↓ BEL, U+0007
|
||||||
|
control0 = abcdef
|
||||||
|
|
||||||
|
# ↓ DEL, U+007F
|
||||||
|
delete = abcdef
|
||||||
|
|
||||||
|
# ↓ BPM, U+0082
|
||||||
|
control1 = abcdef
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "control0",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 28,
|
||||||
|
"end": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "abc\u0007def",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 39,
|
||||||
|
"end": 46
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 39,
|
||||||
|
"end": 46
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": " ↓ BEL, U+0007",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 27
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 46
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "delete",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 74,
|
||||||
|
"end": 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "abcdef",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 83,
|
||||||
|
"end": 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 83,
|
||||||
|
"end": 90
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": " ↓ DEL, U+007F",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 48,
|
||||||
|
"end": 73
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 48,
|
||||||
|
"end": 90
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "control1",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 120,
|
||||||
|
"end": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "abcdef",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 131,
|
||||||
|
"end": 138
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 131,
|
||||||
|
"end": 138
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": " ↓ BPM, U+0082",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 92,
|
||||||
|
"end": 119
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 92,
|
||||||
|
"end": 138
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 139
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
face-with-tears-of-joy = 😂
|
||||||
|
tetragram-for-centre = 𝌆
|
||||||
|
|
||||||
|
surrogates-in-text = \uD83D\uDE02
|
||||||
|
surrogates-in-string = {"\uD83D\uDE02"}
|
||||||
|
surrogates-in-adjacent-strings = {"\uD83D"}{"\uDE02"}
|
||||||
|
|
||||||
|
emoji-in-text = A face 😂 with tears of joy.
|
||||||
|
emoji-in-string = {"A face 😂 with tears of joy."}
|
||||||
|
|
||||||
|
# ERROR Invalid identifier
|
||||||
|
err-😂 = Value
|
||||||
|
|
||||||
|
# ERROR Invalid expression
|
||||||
|
err-invalid-expression = { 😂 }
|
||||||
|
|
||||||
|
# ERROR Invalid variant key
|
||||||
|
err-invalid-variant-key = { $sel ->
|
||||||
|
*[😂] Value
|
||||||
|
}
|
||||||
414
fluent.syntax/src/test/resources/reference_fixtures/astral.json
Normal file
414
fluent.syntax/src/test/resources/reference_fixtures/astral.json
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "face-with-tears-of-joy",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 22
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "😂",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 25,
|
||||||
|
"end": 27
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 25,
|
||||||
|
"end": 27
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 27
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "tetragram-for-centre",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 28,
|
||||||
|
"end": 48
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "𝌆",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 51,
|
||||||
|
"end": 53
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 51,
|
||||||
|
"end": 53
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 28,
|
||||||
|
"end": 53
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "surrogates-in-text",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 55,
|
||||||
|
"end": 73
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "\\uD83D\\uDE02",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 76,
|
||||||
|
"end": 88
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 76,
|
||||||
|
"end": 88
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 55,
|
||||||
|
"end": 88
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "surrogates-in-string",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 89,
|
||||||
|
"end": 109
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\uD83D\\uDE02",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 113,
|
||||||
|
"end": 127
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 112,
|
||||||
|
"end": 128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 112,
|
||||||
|
"end": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 89,
|
||||||
|
"end": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "surrogates-in-adjacent-strings",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 129,
|
||||||
|
"end": 159
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\uD83D",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 163,
|
||||||
|
"end": 171
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 162,
|
||||||
|
"end": 172
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\uDE02",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 173,
|
||||||
|
"end": 181
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 172,
|
||||||
|
"end": 182
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 162,
|
||||||
|
"end": 182
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 129,
|
||||||
|
"end": 182
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "emoji-in-text",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 184,
|
||||||
|
"end": 197
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "A face 😂 with tears of joy.",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 200,
|
||||||
|
"end": 228
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 200,
|
||||||
|
"end": 228
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 184,
|
||||||
|
"end": 228
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "emoji-in-string",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 229,
|
||||||
|
"end": 244
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "A face 😂 with tears of joy.",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 248,
|
||||||
|
"end": 278
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 247,
|
||||||
|
"end": 279
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 247,
|
||||||
|
"end": 279
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 229,
|
||||||
|
"end": 279
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Invalid identifier",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 281,
|
||||||
|
"end": 307
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"="
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"=\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 312,
|
||||||
|
"end": 312
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err-😂 = Value\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 308,
|
||||||
|
"end": 324
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Invalid expression",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 324,
|
||||||
|
"end": 350
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0028",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Expected an inline expression",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 378,
|
||||||
|
"end": 378
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err-invalid-expression = { 😂 }\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 351,
|
||||||
|
"end": 384
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Invalid variant key",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 384,
|
||||||
|
"end": 411
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0004",
|
||||||
|
"arguments": [
|
||||||
|
"a-zA-Z"
|
||||||
|
],
|
||||||
|
"message": "Expected a character from range: \"a-zA-Z\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 454,
|
||||||
|
"end": 454
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err-invalid-variant-key = { $sel ->\n *[😂] Value\n}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 412,
|
||||||
|
"end": 466
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 466
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
## Function names
|
||||||
|
|
||||||
|
valid-func-name-01 = {FUN1()}
|
||||||
|
valid-func-name-02 = {FUN_FUN()}
|
||||||
|
valid-func-name-03 = {FUN-FUN()}
|
||||||
|
|
||||||
|
# JUNK 0 is not a valid Identifier start
|
||||||
|
invalid-func-name-01 = {0FUN()}
|
||||||
|
# JUNK Function names may not be lowercase
|
||||||
|
invalid-func-name-02 = {fun()}
|
||||||
|
# JUNK Function names may not contain lowercase character
|
||||||
|
invalid-func-name-03 = {Fun()}
|
||||||
|
# JUNK ? is not a valid Identifier character
|
||||||
|
invalid-func-name-04 = {FUN?()}
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
positional-args = {FUN(1, "a", msg)}
|
||||||
|
named-args = {FUN(x: 1, y: "Y")}
|
||||||
|
dense-named-args = {FUN(x:1, y:"Y")}
|
||||||
|
mixed-args = {FUN(1, "a", msg, x: 1, y: "Y")}
|
||||||
|
|
||||||
|
# ERROR Positional arg must not follow keyword args
|
||||||
|
shuffled-args = {FUN(1, x: 1, "a", y: "Y", msg)}
|
||||||
|
|
||||||
|
# ERROR Named arguments must be unique
|
||||||
|
duplicate-named-args = {FUN(x: 1, x: "X")}
|
||||||
|
|
||||||
|
|
||||||
|
## Whitespace around arguments
|
||||||
|
|
||||||
|
sparse-inline-call = {FUN ( "a" , msg, x: 1 )}
|
||||||
|
empty-inline-call = {FUN( )}
|
||||||
|
multiline-call = {FUN(
|
||||||
|
"a",
|
||||||
|
msg,
|
||||||
|
x: 1
|
||||||
|
)}
|
||||||
|
sparse-multiline-call = {FUN
|
||||||
|
(
|
||||||
|
|
||||||
|
"a" ,
|
||||||
|
msg
|
||||||
|
, x: 1
|
||||||
|
)}
|
||||||
|
empty-multiline-call = {FUN(
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
unindented-arg-number = {FUN(
|
||||||
|
1)}
|
||||||
|
|
||||||
|
unindented-arg-string = {FUN(
|
||||||
|
"a")}
|
||||||
|
|
||||||
|
unindented-arg-msg-ref = {FUN(
|
||||||
|
msg)}
|
||||||
|
|
||||||
|
unindented-arg-term-ref = {FUN(
|
||||||
|
-msg)}
|
||||||
|
|
||||||
|
unindented-arg-var-ref = {FUN(
|
||||||
|
$var)}
|
||||||
|
|
||||||
|
unindented-arg-call = {FUN(
|
||||||
|
OTHER())}
|
||||||
|
|
||||||
|
unindented-named-arg = {FUN(
|
||||||
|
x:1)}
|
||||||
|
|
||||||
|
unindented-closing-paren = {FUN(
|
||||||
|
x
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Optional trailing comma
|
||||||
|
|
||||||
|
one-argument = {FUN(1,)}
|
||||||
|
many-arguments = {FUN(1, 2, 3,)}
|
||||||
|
inline-sparse-args = {FUN( 1, 2, 3, )}
|
||||||
|
mulitline-args = {FUN(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
|
mulitline-sparse-args = {FUN(
|
||||||
|
|
||||||
|
1
|
||||||
|
,
|
||||||
|
2
|
||||||
|
,
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
## Syntax errors for trailing comma
|
||||||
|
|
||||||
|
one-argument = {FUN(1,,)}
|
||||||
|
missing-arg = {FUN(,)}
|
||||||
|
missing-sparse-arg = {FUN( , )}
|
||||||
|
|
||||||
|
|
||||||
|
## Whitespace in named arguments
|
||||||
|
|
||||||
|
sparse-named-arg = {FUN(
|
||||||
|
x : 1,
|
||||||
|
y : 2,
|
||||||
|
z
|
||||||
|
:
|
||||||
|
3
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
unindented-colon = {FUN(
|
||||||
|
x
|
||||||
|
:1)}
|
||||||
|
|
||||||
|
unindented-value = {FUN(
|
||||||
|
x:
|
||||||
|
1)}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
## Callees in placeables.
|
||||||
|
|
||||||
|
function-callee-placeable = {FUNCTION()}
|
||||||
|
term-callee-placeable = {-term()}
|
||||||
|
|
||||||
|
# ERROR Messages cannot be parameterized.
|
||||||
|
message-callee-placeable = {message()}
|
||||||
|
# ERROR Equivalent to a MessageReference callee.
|
||||||
|
mixed-case-callee-placeable = {Function()}
|
||||||
|
# ERROR Message attributes cannot be parameterized.
|
||||||
|
message-attr-callee-placeable = {message.attr()}
|
||||||
|
# ERROR Term attributes may not be used in Placeables.
|
||||||
|
term-attr-callee-placeable = {-term.attr()}
|
||||||
|
# ERROR Variables cannot be parameterized.
|
||||||
|
variable-callee-placeable = {$variable()}
|
||||||
|
|
||||||
|
|
||||||
|
## Callees in selectors.
|
||||||
|
|
||||||
|
function-callee-selector = {FUNCTION() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
|
term-attr-callee-selector = {-term.attr() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# ERROR Messages cannot be parameterized.
|
||||||
|
message-callee-selector = {message() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
|
# ERROR Equivalent to a MessageReference callee.
|
||||||
|
mixed-case-callee-selector = {Function() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
|
# ERROR Message attributes cannot be parameterized.
|
||||||
|
message-attr-callee-selector = {message.attr() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
|
# ERROR Term values may not be used as selectors.
|
||||||
|
term-callee-selector = {-term() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
|
# ERROR Variables cannot be parameterized.
|
||||||
|
variable-callee-selector = {$variable() ->
|
||||||
|
*[key] Value
|
||||||
|
}
|
||||||
@@ -0,0 +1,706 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "Callees in placeables.",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "function-callee-placeable",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 27,
|
||||||
|
"end": 52
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"type": "FunctionReference",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "FUNCTION",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 56,
|
||||||
|
"end": 64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arguments": {
|
||||||
|
"type": "CallArguments",
|
||||||
|
"positional": [],
|
||||||
|
"named": [],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 64,
|
||||||
|
"end": 66
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 56,
|
||||||
|
"end": 66
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 55,
|
||||||
|
"end": 67
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 55,
|
||||||
|
"end": 67
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 27,
|
||||||
|
"end": 67
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "term-callee-placeable",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 68,
|
||||||
|
"end": 89
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"type": "TermReference",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 94,
|
||||||
|
"end": 98
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attribute": null,
|
||||||
|
"arguments": {
|
||||||
|
"type": "CallArguments",
|
||||||
|
"positional": [],
|
||||||
|
"named": [],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 98,
|
||||||
|
"end": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 93,
|
||||||
|
"end": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 92,
|
||||||
|
"end": 101
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 92,
|
||||||
|
"end": 101
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 68,
|
||||||
|
"end": 101
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Messages cannot be parameterized.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 103,
|
||||||
|
"end": 144
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0008",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "The callee has to be an upper-case identifier or a term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 180,
|
||||||
|
"end": 180
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "message-callee-placeable = {message()}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 145,
|
||||||
|
"end": 184
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Equivalent to a MessageReference callee.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 184,
|
||||||
|
"end": 232
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0008",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "The callee has to be an upper-case identifier or a term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 272,
|
||||||
|
"end": 272
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "mixed-case-callee-placeable = {Function()}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 233,
|
||||||
|
"end": 276
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Message attributes cannot be parameterized.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 276,
|
||||||
|
"end": 327
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 373,
|
||||||
|
"end": 373
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "message-attr-callee-placeable = {message.attr()}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 328,
|
||||||
|
"end": 377
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Term attributes may not be used in Placeables.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 377,
|
||||||
|
"end": 431
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0019",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Attributes of terms cannot be used as placeables",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 474,
|
||||||
|
"end": 474
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "term-attr-callee-placeable = {-term.attr()}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 432,
|
||||||
|
"end": 476
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Variables cannot be parameterized.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 476,
|
||||||
|
"end": 518
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 557,
|
||||||
|
"end": 557
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "variable-callee-placeable = {$variable()}\n\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 519,
|
||||||
|
"end": 563
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Callees in selectors.",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 563,
|
||||||
|
"end": 587
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "function-callee-selector",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 589,
|
||||||
|
"end": 613
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"type": "SelectExpression",
|
||||||
|
"selector": {
|
||||||
|
"type": "FunctionReference",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "FUNCTION",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 617,
|
||||||
|
"end": 625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arguments": {
|
||||||
|
"type": "CallArguments",
|
||||||
|
"positional": [],
|
||||||
|
"named": [],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 625,
|
||||||
|
"end": 627
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 617,
|
||||||
|
"end": 627
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"type": "Variant",
|
||||||
|
"key": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "key",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 636,
|
||||||
|
"end": 639
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 641,
|
||||||
|
"end": 646
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 641,
|
||||||
|
"end": 646
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": true,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 634,
|
||||||
|
"end": 646
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 617,
|
||||||
|
"end": 647
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 616,
|
||||||
|
"end": 648
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 616,
|
||||||
|
"end": 648
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 589,
|
||||||
|
"end": 648
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "term-attr-callee-selector",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 649,
|
||||||
|
"end": 674
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"type": "SelectExpression",
|
||||||
|
"selector": {
|
||||||
|
"type": "TermReference",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 679,
|
||||||
|
"end": 683
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attribute": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "attr",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 684,
|
||||||
|
"end": 688
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arguments": {
|
||||||
|
"type": "CallArguments",
|
||||||
|
"positional": [],
|
||||||
|
"named": [],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 688,
|
||||||
|
"end": 690
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 678,
|
||||||
|
"end": 690
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"type": "Variant",
|
||||||
|
"key": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "key",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 699,
|
||||||
|
"end": 702
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 704,
|
||||||
|
"end": 709
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 704,
|
||||||
|
"end": 709
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": true,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 697,
|
||||||
|
"end": 709
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 678,
|
||||||
|
"end": 710
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 677,
|
||||||
|
"end": 711
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 677,
|
||||||
|
"end": 711
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 649,
|
||||||
|
"end": 711
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Messages cannot be parameterized.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 713,
|
||||||
|
"end": 754
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0008",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "The callee has to be an upper-case identifier or a term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 789,
|
||||||
|
"end": 789
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "message-callee-selector = {message() ->\n *[key] Value\n}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 755,
|
||||||
|
"end": 813
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Equivalent to a MessageReference callee.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 813,
|
||||||
|
"end": 861
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0008",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "The callee has to be an upper-case identifier or a term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 900,
|
||||||
|
"end": 900
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "mixed-case-callee-selector = {Function() ->\n *[key] Value\n}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 862,
|
||||||
|
"end": 924
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Message attributes cannot be parameterized.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 924,
|
||||||
|
"end": 975
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1020,
|
||||||
|
"end": 1020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "message-attr-callee-selector = {message.attr() ->\n *[key] Value\n}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 976,
|
||||||
|
"end": 1044
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Term values may not be used as selectors.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1044,
|
||||||
|
"end": 1093
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0017",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Terms cannot be used as selectors",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1126,
|
||||||
|
"end": 1126
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "term-callee-selector = {-term() ->\n *[key] Value\n}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1094,
|
||||||
|
"end": 1147
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Variables cannot be parameterized.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1147,
|
||||||
|
"end": 1189
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1227,
|
||||||
|
"end": 1227
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "variable-callee-selector = {$variable() ->\n *[key] Value\n}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1190,
|
||||||
|
"end": 1251
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 1251
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Standalone Comment
|
||||||
|
|
||||||
|
# Message Comment
|
||||||
|
foo = Foo
|
||||||
|
|
||||||
|
# Term Comment
|
||||||
|
# with a blank last line.
|
||||||
|
#
|
||||||
|
-term = Term
|
||||||
|
|
||||||
|
# Another standalone
|
||||||
|
#
|
||||||
|
# with indent
|
||||||
|
## Group Comment
|
||||||
|
### Resource Comment
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
#error
|
||||||
|
##error
|
||||||
|
###error
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "Standalone Comment",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "foo",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 40,
|
||||||
|
"end": 43
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Foo",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 46,
|
||||||
|
"end": 49
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 46,
|
||||||
|
"end": 49
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": "Message Comment",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 22,
|
||||||
|
"end": 39
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 22,
|
||||||
|
"end": 49
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Term",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 95,
|
||||||
|
"end": 99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Term",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 102,
|
||||||
|
"end": 106
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 102,
|
||||||
|
"end": 106
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": "Term Comment\nwith a blank last line.\n",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 51,
|
||||||
|
"end": 93
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 51,
|
||||||
|
"end": 106
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Another standalone\n\n with indent",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 108,
|
||||||
|
"end": 150
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Group Comment",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 151,
|
||||||
|
"end": 167
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Resource Comment",
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 168,
|
||||||
|
"end": 188
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Errors",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 190,
|
||||||
|
"end": 198
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
"message": "Expected token: \" \"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 200,
|
||||||
|
"end": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "#error\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 199,
|
||||||
|
"end": 206
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
"message": "Expected token: \" \"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 208,
|
||||||
|
"end": 208
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "##error\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 206,
|
||||||
|
"end": 214
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
"message": "Expected token: \" \"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 217,
|
||||||
|
"end": 217
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "###error\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 214,
|
||||||
|
"end": 223
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 223
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
### This entire file uses CR as EOL.
|
||||||
19
fluent.syntax/src/test/resources/reference_fixtures/cr.json
Normal file
19
fluent.syntax/src/test/resources/reference_fixtures/cr.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"content": "This entire file uses CR as EOL.\r\rerr01 = Value 01\rerr02 = Value 02\r\rerr03 =\r\r Value 03\r Continued\r\r .title = Title\r\rerr04 = { \"str\r\rerr05 = { $sel -> }\r",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 166
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 166
|
||||||
|
}
|
||||||
|
}
|
||||||
14
fluent.syntax/src/test/resources/reference_fixtures/crlf.ftl
Normal file
14
fluent.syntax/src/test/resources/reference_fixtures/crlf.ftl
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
key01 = Value 01
|
||||||
|
key02 =
|
||||||
|
|
||||||
|
Value 02
|
||||||
|
Continued
|
||||||
|
|
||||||
|
.title = Title
|
||||||
|
|
||||||
|
# ERROR Unclosed StringLiteral
|
||||||
|
err03 = { "str
|
||||||
|
|
||||||
|
# ERROR Missing newline after ->.
|
||||||
|
err04 = { $sel -> }
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "key01"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value 01"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "key02"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value 02\nContinued"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"type": "Attribute",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "title"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Title"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"comment": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Comment",
|
||||||
|
"content": "ERROR Unclosed StringLiteral"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [],
|
||||||
|
"content": "err03 = { \"str\r\n\r\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Comment",
|
||||||
|
"content": "ERROR Missing newline after ->."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [],
|
||||||
|
"content": "err04 = { $sel -> }\r\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### NOTE: Disable final newline insertion when editing this file.
|
||||||
|
|
||||||
|
# No EOL
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "NOTE: Disable final newline insertion when editing this file.",
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 65
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "No EOL",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 75
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### NOTE: Disable final newline insertion when editing this file.
|
||||||
|
|
||||||
|
message-id
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "NOTE: Disable final newline insertion when editing this file.",
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 65
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"="
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"=\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 77,
|
||||||
|
"end": 77
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "message-id",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 77
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 77
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### NOTE: Disable final newline insertion when editing this file.
|
||||||
|
|
||||||
|
message-id =
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "NOTE: Disable final newline insertion when editing this file.",
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 65
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0005",
|
||||||
|
"arguments": [
|
||||||
|
"message-id"
|
||||||
|
],
|
||||||
|
"message": "Expected message \"message-id\" to have a value or attributes",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 79,
|
||||||
|
"end": 79
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "message-id =",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 79
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 79
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### NOTE: Disable final newline insertion when editing this file.
|
||||||
|
|
||||||
|
000
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "NOTE: Disable final newline insertion when editing this file.",
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 65
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0002",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Expected an entry start",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 67
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "000",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 70
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### NOTE: Disable final newline insertion when editing this file.
|
||||||
|
|
||||||
|
no-eol = No EOL
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "NOTE: Disable final newline insertion when editing this file.",
|
||||||
|
"type": "ResourceComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 65
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "no-eol",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 73
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "No EOL",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 76,
|
||||||
|
"end": 82
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 76,
|
||||||
|
"end": 82
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 82
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 82
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
## Literal text
|
||||||
|
text-backslash-one = Value with \ a backslash
|
||||||
|
text-backslash-two = Value with \\ two backslashes
|
||||||
|
text-backslash-brace = Value with \{placeable}
|
||||||
|
text-backslash-u = \u0041
|
||||||
|
text-backslash-backslash-u = \\u0041
|
||||||
|
|
||||||
|
## String literals
|
||||||
|
quote-in-string = {"\""}
|
||||||
|
backslash-in-string = {"\\"}
|
||||||
|
# ERROR Mismatched quote
|
||||||
|
mismatched-quote = {"\\""}
|
||||||
|
# ERROR Unknown escape
|
||||||
|
unknown-escape = {"\x"}
|
||||||
|
# ERROR Multiline literal
|
||||||
|
invalid-multiline-literal = {"
|
||||||
|
"}
|
||||||
|
|
||||||
|
## Unicode escapes
|
||||||
|
string-unicode-4digits = {"\u0041"}
|
||||||
|
escape-unicode-4digits = {"\\u0041"}
|
||||||
|
string-unicode-6digits = {"\U01F602"}
|
||||||
|
escape-unicode-6digits = {"\\U01F602"}
|
||||||
|
|
||||||
|
# OK The trailing "00" is part of the literal value.
|
||||||
|
string-too-many-4digits = {"\u004100"}
|
||||||
|
# OK The trailing "00" is part of the literal value.
|
||||||
|
string-too-many-6digits = {"\U01F60200"}
|
||||||
|
|
||||||
|
# ERROR Too few hex digits after \u.
|
||||||
|
string-too-few-4digits = {"\u41"}
|
||||||
|
# ERROR Too few hex digits after \U.
|
||||||
|
string-too-few-6digits = {"\U1F602"}
|
||||||
|
|
||||||
|
## Literal braces
|
||||||
|
brace-open = An opening {"{"} brace.
|
||||||
|
brace-close = A closing {"}"} brace.
|
||||||
@@ -0,0 +1,937 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "Literal text",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "text-backslash-one",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 16,
|
||||||
|
"end": 34
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value with \\ a backslash",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 37,
|
||||||
|
"end": 61
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 37,
|
||||||
|
"end": 61
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 16,
|
||||||
|
"end": 61
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "text-backslash-two",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 62,
|
||||||
|
"end": 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value with \\\\ two backslashes",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 83,
|
||||||
|
"end": 112
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 83,
|
||||||
|
"end": 112
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 62,
|
||||||
|
"end": 112
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "text-backslash-brace",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 113,
|
||||||
|
"end": 133
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "Value with \\",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 136,
|
||||||
|
"end": 148
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"type": "MessageReference",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "placeable",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 149,
|
||||||
|
"end": 158
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attribute": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 149,
|
||||||
|
"end": 158
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 148,
|
||||||
|
"end": 159
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 136,
|
||||||
|
"end": 159
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 113,
|
||||||
|
"end": 159
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "text-backslash-u",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 160,
|
||||||
|
"end": 176
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "\\u0041",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 179,
|
||||||
|
"end": 185
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 179,
|
||||||
|
"end": 185
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 160,
|
||||||
|
"end": 185
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "text-backslash-backslash-u",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 186,
|
||||||
|
"end": 212
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "\\\\u0041",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 215,
|
||||||
|
"end": 222
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 215,
|
||||||
|
"end": 222
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 186,
|
||||||
|
"end": 222
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "String literals",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 224,
|
||||||
|
"end": 242
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "quote-in-string",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 243,
|
||||||
|
"end": 258
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\\"",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 262,
|
||||||
|
"end": 266
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 261,
|
||||||
|
"end": 267
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 261,
|
||||||
|
"end": 267
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 243,
|
||||||
|
"end": 267
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "backslash-in-string",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 268,
|
||||||
|
"end": 287
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\\\",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 291,
|
||||||
|
"end": 295
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 290,
|
||||||
|
"end": 296
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 290,
|
||||||
|
"end": 296
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 268,
|
||||||
|
"end": 296
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Mismatched quote",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 297,
|
||||||
|
"end": 321
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 346,
|
||||||
|
"end": 346
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "mismatched-quote = {\"\\\\\"\"}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 322,
|
||||||
|
"end": 349
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Unknown escape",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 349,
|
||||||
|
"end": 371
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0025",
|
||||||
|
"arguments": [
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
"message": "Unknown escape sequence: \\x.",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 392,
|
||||||
|
"end": 392
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "unknown-escape = {\"\\x\"}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 372,
|
||||||
|
"end": 396
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Multiline literal",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 396,
|
||||||
|
"end": 421
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0020",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Unterminated string expression",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 452,
|
||||||
|
"end": 452
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "invalid-multiline-literal = {\"\n \"}\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 422,
|
||||||
|
"end": 458
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Unicode escapes",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 458,
|
||||||
|
"end": 476
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "string-unicode-4digits",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 477,
|
||||||
|
"end": 499
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\u0041",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 503,
|
||||||
|
"end": 511
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 502,
|
||||||
|
"end": 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 502,
|
||||||
|
"end": 512
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 477,
|
||||||
|
"end": 512
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "escape-unicode-4digits",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 513,
|
||||||
|
"end": 535
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\\\u0041",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 539,
|
||||||
|
"end": 548
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 538,
|
||||||
|
"end": 549
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 538,
|
||||||
|
"end": 549
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 513,
|
||||||
|
"end": 549
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "string-unicode-6digits",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 550,
|
||||||
|
"end": 572
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\U01F602",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 576,
|
||||||
|
"end": 586
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 575,
|
||||||
|
"end": 587
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 575,
|
||||||
|
"end": 587
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 550,
|
||||||
|
"end": 587
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "escape-unicode-6digits",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 588,
|
||||||
|
"end": 610
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\\\U01F602",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 614,
|
||||||
|
"end": 625
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 613,
|
||||||
|
"end": 626
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 613,
|
||||||
|
"end": 626
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 588,
|
||||||
|
"end": 626
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "string-too-many-4digits",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 681,
|
||||||
|
"end": 704
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\u004100",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 708,
|
||||||
|
"end": 718
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 707,
|
||||||
|
"end": 719
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 707,
|
||||||
|
"end": 719
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": "OK The trailing \"00\" is part of the literal value.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 628,
|
||||||
|
"end": 680
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 628,
|
||||||
|
"end": 719
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "string-too-many-6digits",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 773,
|
||||||
|
"end": 796
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "\\U01F60200",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 800,
|
||||||
|
"end": 812
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 799,
|
||||||
|
"end": 813
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 799,
|
||||||
|
"end": 813
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": {
|
||||||
|
"content": "OK The trailing \"00\" is part of the literal value.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 720,
|
||||||
|
"end": 772
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 720,
|
||||||
|
"end": 813
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Too few hex digits after \\u.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 815,
|
||||||
|
"end": 851
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0026",
|
||||||
|
"arguments": [
|
||||||
|
"\\u41\""
|
||||||
|
],
|
||||||
|
"message": "Invalid Unicode escape sequence: \\u41\".",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 883,
|
||||||
|
"end": 883
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string-too-few-4digits = {\"\\u41\"}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 852,
|
||||||
|
"end": 886
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "ERROR Too few hex digits after \\U.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 886,
|
||||||
|
"end": 922
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0026",
|
||||||
|
"arguments": [
|
||||||
|
"\\U1F602\""
|
||||||
|
],
|
||||||
|
"message": "Invalid Unicode escape sequence: \\U1F602\".",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 957,
|
||||||
|
"end": 957
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string-too-few-6digits = {\"\\U1F602\"}\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 923,
|
||||||
|
"end": 961
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Literal braces",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 961,
|
||||||
|
"end": 978
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "brace-open",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 979,
|
||||||
|
"end": 989
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "An opening ",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 992,
|
||||||
|
"end": 1003
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "{",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1004,
|
||||||
|
"end": 1007
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1003,
|
||||||
|
"end": 1008
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": " brace.",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1008,
|
||||||
|
"end": 1015
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 992,
|
||||||
|
"end": 1015
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 979,
|
||||||
|
"end": 1015
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Message",
|
||||||
|
"id": {
|
||||||
|
"type": "Identifier",
|
||||||
|
"name": "brace-close",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1016,
|
||||||
|
"end": 1027
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "Pattern",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": "A closing ",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1030,
|
||||||
|
"end": 1040
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Placeable",
|
||||||
|
"expression": {
|
||||||
|
"value": "}",
|
||||||
|
"type": "StringLiteral",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1041,
|
||||||
|
"end": 1044
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1040,
|
||||||
|
"end": 1045
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextElement",
|
||||||
|
"value": " brace.",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1045,
|
||||||
|
"end": 1052
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1030,
|
||||||
|
"end": 1052
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": [],
|
||||||
|
"comment": null,
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 1016,
|
||||||
|
"end": 1052
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 1053
|
||||||
|
}
|
||||||
|
}
|
||||||
21
fluent.syntax/src/test/resources/reference_fixtures/junk.ftl
Normal file
21
fluent.syntax/src/test/resources/reference_fixtures/junk.ftl
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## Two adjacent Junks.
|
||||||
|
err01 = {1x}
|
||||||
|
err02 = {2x}
|
||||||
|
|
||||||
|
# A single Junk.
|
||||||
|
err03 = {1x
|
||||||
|
2
|
||||||
|
|
||||||
|
# A single Junk.
|
||||||
|
ą=Invalid identifier
|
||||||
|
ć=Another one
|
||||||
|
|
||||||
|
# The COMMENT ends this junk.
|
||||||
|
err04 = {
|
||||||
|
# COMMENT
|
||||||
|
|
||||||
|
# The COMMENT ends this junk.
|
||||||
|
# The closing brace is a separate Junk.
|
||||||
|
err04 = {
|
||||||
|
# COMMENT
|
||||||
|
}
|
||||||
233
fluent.syntax/src/test/resources/reference_fixtures/junk.json
Normal file
233
fluent.syntax/src/test/resources/reference_fixtures/junk.json
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
{
|
||||||
|
"type": "Resource",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"content": "Two adjacent Junks.",
|
||||||
|
"type": "GroupComment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 22
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 33,
|
||||||
|
"end": 33
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err01 = {1x}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 23,
|
||||||
|
"end": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 46,
|
||||||
|
"end": 46
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err02 = {2x}\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 36,
|
||||||
|
"end": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "A single Junk.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 50,
|
||||||
|
"end": 66
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0003",
|
||||||
|
"arguments": [
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"message": "Expected token: \"}\"",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 77,
|
||||||
|
"end": 77
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err03 = {1x\n2\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 67,
|
||||||
|
"end": 82
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "A single Junk.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 82,
|
||||||
|
"end": 98
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0002",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Expected an entry start",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 99,
|
||||||
|
"end": 99
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "ą=Invalid identifier\nć=Another one\n\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 99,
|
||||||
|
"end": 135
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "The COMMENT ends this junk.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 135,
|
||||||
|
"end": 164
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0028",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Expected an inline expression",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 175,
|
||||||
|
"end": 175
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err04 = {\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 165,
|
||||||
|
"end": 175
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "COMMENT",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 175,
|
||||||
|
"end": 184
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "The COMMENT ends this junk.\nThe closing brace is a separate Junk.",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 186,
|
||||||
|
"end": 255
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0028",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Expected an inline expression",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 266,
|
||||||
|
"end": 266
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "err04 = {\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 256,
|
||||||
|
"end": 266
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "COMMENT",
|
||||||
|
"type": "Comment",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 266,
|
||||||
|
"end": 275
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Junk",
|
||||||
|
"annotations": [
|
||||||
|
{
|
||||||
|
"type": "Annotation",
|
||||||
|
"code": "E0002",
|
||||||
|
"arguments": [],
|
||||||
|
"message": "Expected an entry start",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 276,
|
||||||
|
"end": 276
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "}\n",
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 276,
|
||||||
|
"end": 278
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"span": {
|
||||||
|
"type": "Span",
|
||||||
|
"start": 0,
|
||||||
|
"end": 278
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user