Skip to content

Commit 8308f8b

Browse files
Merge branch 'master' into feat/better-issues
2 parents 57f2b15 + ad17a02 commit 8308f8b

File tree

5 files changed

+132
-2
lines changed

5 files changed

+132
-2
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
- `Issue`'s messages are capitalized.
1010
- Parsing `Issue`s' position uses the token's length.
1111

12+
## [1.6.30] – 2024-09-30
13+
14+
### Added
15+
- `Node.transformChildren` to run a function on all the children of a Node
16+
- `Node.replaceWith` to replace a Node with another
17+
18+
### Changed
19+
- Aligned Token Categories with Kolasu
20+
- Increased code coverage
21+
1222
## [1.6.29] – 2024-07-09
1323

1424
### Fixed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "AST building blocks for TypeScript/JavaScript, part of the *lasu family, with optional integrations with ANTLR4 and Ecore.",
44
"author": "Strumenta s.r.l.",
55
"publisher": "strumenta",
6-
"version": "1.6.29",
6+
"version": "1.6.30",
77
"license": "Apache-2.0",
88
"keywords": [
99
"antlr",

src/model/model.ts

+51
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,57 @@ export abstract class Node extends Origin implements Destination {
340340
return this;
341341
}
342342

343+
/**
344+
* Replace the current Node with another Node
345+
*
346+
* @throws Error - if this.parent === undefined
347+
*/
348+
replaceWith(other: Node): void {
349+
if (!this.parent) {
350+
throw new Error("Cannot replace a Node that has no parent")
351+
}
352+
353+
this.parent.transformChildren(node => node === this ? other : node);
354+
}
355+
356+
/**
357+
* Apply the given `operation` function to
358+
* all of the Node's children.
359+
*
360+
* It's possible to use both pure functions,
361+
* to replace the nodes, and functions that mutate
362+
* the nodes in-place.
363+
*/
364+
transformChildren(operation: (node: Node) => Node): void {
365+
const nodesNames = this.getChildNames();
366+
367+
nodesNames.forEach(nodeName => {
368+
const propertyValue = this[nodeName];
369+
if (propertyValue instanceof Node) {
370+
const newNode = operation(propertyValue);
371+
/*
372+
* Identity check;
373+
* If the variable is pointing to the same object,
374+
* the operation changed the Node in-place.
375+
*/
376+
if (newNode !== propertyValue) {
377+
this.setChild(nodeName, newNode);
378+
}
379+
380+
} else if (Array.isArray(propertyValue)) {
381+
propertyValue.forEach((element, index) => {
382+
if (element instanceof Node) {
383+
const newNode = operation(element);
384+
// Another identity check
385+
if (newNode !== element) {
386+
(propertyValue as Node[])[index] = newNode;
387+
}
388+
}
389+
});
390+
}
391+
});
392+
}
393+
343394
withOrigin(origin?: Origin): this {
344395
this.origin = origin;
345396
return this;

src/parsing/parsing.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ export enum TokenCategory {
1010
KEYWORD = "Keyword",
1111
NUMERIC_LITERAL = "Numeric literal",
1212
STRING_LITERAL = "String literal",
13-
PLAIN_TEXT = "Plain text"
13+
OTHER_LITERAL = "Other literal",
14+
PLAIN_TEXT = "Plain text",
15+
WHITESPACE = "Whitespace",
16+
IDENTIFIER = "Identifier",
17+
PUNCTUATION = "Punctuation",
18+
OPERATOR = "Operator",
1419
}
1520

1621
/**

tests/nodes/nodes.test.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { expect } from "chai";
2+
import { Box, SomeNode, SomeNodeInPackage } from "../nodes";
3+
4+
describe('Node.transformChildren', () => {
5+
let childNode1: SomeNode;
6+
let childNode2: SomeNode;
7+
let boxNode: Box;
8+
9+
beforeEach(() => {
10+
childNode1 = new SomeNode("Child1");
11+
childNode2 = new SomeNode("Child2");
12+
13+
boxNode = new Box("BoxNode", [childNode1, childNode2]);
14+
})
15+
16+
it('should apply in-place transformation to each child node', () => {
17+
const inPlaceTransformation = (node: SomeNode): SomeNode => {
18+
node.a = node.a?.toUpperCase();
19+
return node;
20+
};
21+
22+
boxNode.transformChildren(inPlaceTransformation);
23+
24+
expect(boxNode.contents[0]["a"]).to.eq("CHILD1");
25+
expect(boxNode.contents[0]).to.eq(childNode1);
26+
expect(boxNode.contents[1]["a"]).to.eq("CHILD2");
27+
expect(boxNode.contents[1]).to.eq(childNode2);
28+
});
29+
30+
it('should replace children nodes when used with pure-functions', () => {
31+
const replaceTransformation = (node: SomeNode): SomeNode => {
32+
return new SomeNode(node.a?.toUpperCase())
33+
};
34+
35+
boxNode.transformChildren(replaceTransformation);
36+
37+
expect(boxNode.contents[0]["a"]).to.eq("CHILD1");
38+
expect(boxNode.contents[0]).to.not.eq(childNode1);
39+
expect(boxNode.contents[1]["a"]).to.eq("CHILD2");
40+
expect(boxNode.contents[1]).to.not.eq(childNode2);
41+
});
42+
});
43+
44+
describe('Node.replaceWith', () => {
45+
it('should replace a child node with another node', () => {
46+
const childNode1 = new SomeNode("Child1");
47+
const childNode2 = new SomeNode("Child2");
48+
49+
const parentNode = new SomeNodeInPackage("ParentNode");
50+
51+
parentNode.setChild('someNode', childNode1);
52+
expect(parentNode.getChildren('someNode')).to.eql([childNode1]);
53+
childNode1.replaceWith(childNode2);
54+
expect(parentNode.getChildren('someNode')).to.eql([childNode2]);
55+
});
56+
57+
it('should throw error if parent is not set', () => {
58+
const childNode1 = new SomeNode("Child1");
59+
60+
const nodeWithoutParent = new SomeNode("NodeWithoutParent");
61+
expect(() => nodeWithoutParent.replaceWith(childNode1)).to.throw('Cannot replace a Node that has no parent');
62+
});
63+
});
64+

0 commit comments

Comments
 (0)