diff --git a/package.json b/package.json index 7742130..5b77735 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/interop/starlasu-v2-metamodel.ts b/src/interop/starlasu-v2-metamodel.ts index 2a330c6..daebc36 100644 --- a/src/interop/starlasu-v2-metamodel.ts +++ b/src/interop/starlasu-v2-metamodel.ts @@ -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({ + 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" diff --git a/src/parsing/tylasu-parser.ts b/src/parsing/tylasu-parser.ts index 340804f..50cd608 100644 --- a/src/parsing/tylasu-parser.ts +++ b/src/parsing/tylasu-parser.ts @@ -67,6 +67,10 @@ export class ANTLRTokenFactory extends TokenFactory { } } +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 implements TylasuLexer { constructor(public readonly tokenFactory: TokenFactory) {} @@ -99,7 +103,8 @@ export abstract class TylasuANTLRLexer 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); @@ -117,7 +122,9 @@ export abstract class TylasuANTLRLexer implements TylasuL Issue.lexical( msg || "unspecified", IssueSeverity.ERROR, - Position.ofPoint(new Point(line, charPositionInLine)))); + Position.ofPoint(new Point(line, charPositionInLine)), + undefined, + SYNTAX_ERROR)); } }); } @@ -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 || "" }, + ])); }); } @@ -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)); } }); } diff --git a/src/transformation/transformation.ts b/src/transformation/transformation.ts index 5b0fcdd..d98d260 100644 --- a/src/transformation/transformation.ts +++ b/src/transformation/transformation.ts @@ -188,6 +188,8 @@ export class ChildNodeFactory { */ const NO_CHILD_NODE = new ChildNodeFactory("", (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 @@ -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})`) } } diff --git a/src/validation.ts b/src/validation.ts index 4633431..15bbd3d 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -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); } -} \ No newline at end of file +} diff --git a/tests/data/playground/example1.json b/tests/data/playground/example1.json index 3da8bcb..64cfc17 100644 --- a/tests/data/playground/example1.json +++ b/tests/data/playground/example1.json @@ -23,7 +23,8 @@ "eClass": "https://strumenta.com/starlasu/v2#//Issue", "type": "SEMANTIC", "message": "Something's wrong", - "severity": "ERROR" + "severity": "ERROR", + "args": [] } ] }, diff --git a/tests/interop/parser-trace.test.ts b/tests/interop/parser-trace.test.ts index 57dfe27..856bc93 100644 --- a/tests/interop/parser-trace.test.ts +++ b/tests/interop/parser-trace.test.ts @@ -32,6 +32,9 @@ describe('Parser traces – Kolasu metamodel V1', function() { expect(trace.issues[0].message).to.eql("Physical line of type FileDescription are currently ignored"); expect(trace.issues[0].severity).to.eql(IssueSeverity.WARNING); expect(trace.issues[0].position).to.eql(new Position(new Point(18, 0), new Point(18, 42))); + expect(trace.issues[0].node).to.be.undefined; + expect(trace.issues[0].code).to.be.undefined; + expect(trace.issues[0].args).to.be.eql([]); const updateStmt = trace.rootNode.findByPosition(pos(258, 30, 258, 30)); expect(updateStmt).not.to.be.undefined; diff --git a/tests/issues.test.ts b/tests/issues.test.ts new file mode 100644 index 0000000..a9f4b09 --- /dev/null +++ b/tests/issues.test.ts @@ -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"); + }); +}); diff --git a/yarn.lock b/yarn.lock index 3d47859..c956055 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"