Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: i18n of issues #70

Merged
merged 4 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"cross-env": "^7.0.3",
"ecore": "^0.12.0",
"eslint": "^8.55.0",
"i18next": "^23.11.5",
"jest": "^29.7.0",
"merge-options": "^2.0.0",
"rimraf": "^3.0.0",
Expand Down
16 changes: 16 additions & 0 deletions src/interop/starlasu-v2-metamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,22 @@ THE_ISSUE_ECLASS.get("eStructuralFeatures").add(ECore.EReference.create({
eType: THE_POSITION_ECLASS,
containment: true
}));
THE_ISSUE_ECLASS.get("eStructuralFeatures").add(ECore.EReference.create({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should bump the version of the metamodel for this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, this is only needed because for some reason in the Ecore compatibility layer we decide that something is an issue if it has the same features as the Issue EClass. Since we're going to abandon ECore anyway, I wouldn't spend much on it.

name: "node",
eType: THE_NODE_ECLASS,
containment: false
}));
THE_ISSUE_ECLASS.get("eStructuralFeatures").add(ECore.EAttribute.create({
name: "code",
eType: ECore.EString,
lowerBound: 1
}));
THE_ISSUE_ECLASS.get("eStructuralFeatures").add(ECore.EAttribute.create({
name: "args",
eType: ECore.EString,
lowerBound: 0,
upperBound: -1
}));

export const THE_RESULT_ECLASS = ECore.EClass.create({
name: "Result"
Expand Down
29 changes: 23 additions & 6 deletions src/parsing/tylasu-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export class ANTLRTokenFactory extends TokenFactory<TylasuANTLRToken> {
}
}

export const SYNTAX_ERROR = "parser.syntaxError";
export const INPUT_NOT_FULLY_CONSUMED = "parser.inputNotFullyConsumed";
export const ERROR_NODE_FOUND = "parser.errorNodeFound";

export abstract class TylasuANTLRLexer<T extends TylasuToken> implements TylasuLexer<T> {

constructor(public readonly tokenFactory: TokenFactory<T>) {}
Expand Down Expand Up @@ -99,7 +103,8 @@ export abstract class TylasuANTLRLexer<T extends TylasuToken> implements TylasuL

if (t && (t.type != Token.EOF)) {
const message = "The lexer didn't consume the entire input";
issues.push(Issue.syntactic(message, IssueSeverity.WARNING, Position.ofTokenEnd(t)))
issues.push(Issue.lexical(message, IssueSeverity.WARNING, Position.ofTokenEnd(t), undefined,
INPUT_NOT_FULLY_CONSUMED))
}

const code = inputStream.getTextFromRange(0, inputStream.size - 1);
Expand All @@ -117,7 +122,9 @@ export abstract class TylasuANTLRLexer<T extends TylasuToken> implements TylasuL
Issue.lexical(
msg || "unspecified",
IssueSeverity.ERROR,
Position.ofPoint(new Point(line, charPositionInLine))));
Position.ofPoint(new Point(line, charPositionInLine)),
undefined,
SYNTAX_ERROR));
}
});
}
Expand Down Expand Up @@ -177,15 +184,23 @@ export abstract class TylasuParser<
Issue.syntactic(
"The whole input was not consumed",
IssueSeverity.ERROR,
Position.ofTokenEnd(lastToken)));
Position.ofTokenEnd(lastToken),
undefined,
INPUT_NOT_FULLY_CONSUMED));
}

processDescendantsAndErrors(
root,
() => {},
it => {
const message = `Error node found (token: ${it.symbol?.text})`;
issues.push(Issue.syntactic(message, IssueSeverity.ERROR, Position.ofParseTree(it)));
const message = `Error node found (token: ${it.symbol?.type} – ${it.symbol?.text})`;
issues.push(Issue.syntactic(message, IssueSeverity.ERROR, Position.ofParseTree(it),
undefined,
ERROR_NODE_FOUND,
[
{ name: "type", value: it.symbol?.type?.toString() || "" },
{ name: "text", value: it.symbol?.text || "" },
]));
});
}

Expand Down Expand Up @@ -270,7 +285,9 @@ export abstract class TylasuParser<
Issue.syntactic(
msg || "unspecified",
IssueSeverity.ERROR,
Position.ofPoint(new Point(line, charPositionInLine))));
Position.ofPoint(new Point(line, charPositionInLine)),
undefined,
SYNTAX_ERROR));
}
});
}
Expand Down
13 changes: 9 additions & 4 deletions src/transformation/transformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ export class ChildNodeFactory<Source, Target, Child> {
*/
const NO_CHILD_NODE = new ChildNodeFactory<any, any, any>("", (node) => node);

export const SOURCE_NODE_NOT_MAPPED = "ast.transform.sourceNotMapped";

/**
* Implementation of a tree-to-tree transformation. For each source node type, we can register a factory that knows how
* to create a transformed node. Then, this transformer can read metadata in the transformed node to recursively
Expand Down Expand Up @@ -264,15 +266,18 @@ export class ASTTransformer {
if (this.allowGenericNode) {
const origin : Origin | undefined = this.asOrigin(source);
nodes = [new GenericNode(parent).withOrigin(origin)];
const nodeType = getNodeDefinition(source)?.name || source?.constructor.name || "–";
this.issues.push(
Issue.semantic(
`Source node not mapped: ${getNodeDefinition(source)?.name}`,
`Source node not mapped: ${nodeType}`,
IssueSeverity.INFO,
origin?.position
origin?.position,
origin instanceof Node ? origin : undefined,
SOURCE_NODE_NOT_MAPPED,
[{ name: "nodeType", value: nodeType }]
)
);
}
else {
} else {
throw new Error(`Unable to translate node ${source} (class ${getNodeDefinition(source)?.name})`)
}
}
Expand Down
46 changes: 29 additions & 17 deletions src/validation.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
import {Node} from "./model/model";
import {Position} from "./model/position";

export enum IssueType { LEXICAL, SYNTACTIC, SEMANTIC}

export enum IssueSeverity { ERROR, WARNING, INFO}

export interface IssueArg {
name: string;
value: string;
}

export class Issue {
type: IssueType;
message: string;
severity: IssueSeverity = IssueSeverity.ERROR;
position?: Position;

constructor(type: IssueType, message: string, severity: IssueSeverity, position?: Position) {
this.type = type;
this.message = message;
this.severity = severity;
this.position = position;

constructor(
public readonly type: IssueType,
public readonly message: string,
public readonly severity: IssueSeverity,
public readonly position?: Position,
public readonly node?: Node,
public readonly code?: string,
public readonly args: IssueArg[] = []
) {
if (!position) {
this.position = node?.position;
}
}

static lexical(message: string, severity: IssueSeverity = IssueSeverity.ERROR, position?: Position): Issue {
return new Issue(IssueType.LEXICAL, message, severity, position);
static lexical(message: string, severity: IssueSeverity = IssueSeverity.ERROR, position?: Position,
node?: Node, code?: string, args: IssueArg[] = []): Issue {
return new Issue(IssueType.LEXICAL, message, severity, position, node, code, args);
}

static syntactic(message: string, severity: IssueSeverity = IssueSeverity.ERROR, position?: Position): Issue {
return new Issue(IssueType.SYNTACTIC, message, severity, position);
static syntactic(message: string, severity: IssueSeverity = IssueSeverity.ERROR, position?: Position,
node?: Node, code?: string, args: IssueArg[] = []): Issue {
return new Issue(IssueType.SYNTACTIC, message, severity, position, node, code, args);
}

static semantic(message: string, severity: IssueSeverity = IssueSeverity.ERROR, position?: Position): Issue {
return new Issue(IssueType.SEMANTIC, message, severity, position);
static semantic(message: string, severity: IssueSeverity = IssueSeverity.ERROR, position?: Position,
node?: Node, code?: string, args: IssueArg[] = []): Issue {
return new Issue(IssueType.SEMANTIC, message, severity, position, node, code, args);
}
}
}
3 changes: 2 additions & 1 deletion tests/data/playground/example1.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"eClass": "https://strumenta.com/starlasu/v2#//Issue",
"type": "SEMANTIC",
"message": "Something's wrong",
"severity": "ERROR"
"severity": "ERROR",
"args": []
}
]
},
Expand Down
33 changes: 33 additions & 0 deletions tests/issues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Issue, IssueSeverity, SOURCE_NODE_NOT_MAPPED} from "../src";
import i18next from 'i18next';
import {SYNTAX_ERROR} from "../src/parsing";
import {expect} from "chai";

describe('Issues', function() {
it("can be translated",
function () {
i18next.init({
lng: 'en', // if you're using a language detector, do not define the lng option
debug: true,
resources: {
en: {
translation: {
ast: {
transform: {
sourceNotMapped: "Source node not mapped: {{type}}"
}
},
parser: {
syntaxError: "A syntax error occurred!"
}
}
}
}
});
let issue = Issue.syntactic("Unexpected token: foo", IssueSeverity.ERROR, undefined, undefined, SYNTAX_ERROR);
expect(i18next.t(issue.code!)).to.equal("A syntax error occurred!");
issue = Issue.semantic("Node not mapped: SomeNode", IssueSeverity.ERROR, undefined, undefined,
SOURCE_NODE_NOT_MAPPED, [{ name: "nodeType", value: "SomeNode" }]);
expect(i18next.t(issue.code!, { type: issue.args[0].value })).to.equal("Source node not mapped: SomeNode");
});
});
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"

"@babel/runtime@^7.23.2":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
dependencies:
regenerator-runtime "^0.14.0"

"@babel/template@^7.22.15", "@babel/template@^7.3.3":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
Expand Down Expand Up @@ -1780,6 +1787,13 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==

i18next@^23.11.5:
version "23.11.5"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.5.tgz#d71eb717a7e65498d87d0594f2664237f9e361ef"
integrity sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==
dependencies:
"@babel/runtime" "^7.23.2"

ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
Expand Down Expand Up @@ -2709,6 +2723,11 @@ reflect-metadata@^0.1.13:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==

regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==

require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
Expand Down