Skip to content

SONARARMOR-817 Parser should serialize TSModuleDeclaration nodes #5226

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

Merged
merged 5 commits into from
Mar 25, 2025

Conversation

roberto-orlandi-sonarsource
Copy link
Contributor

@roberto-orlandi-sonarsource roberto-orlandi-sonarsource commented Mar 20, 2025

SONARARMOR-817

Part of

Base automatically changed from rob/SONARARMOR-820 to master March 24, 2025 08:48
@roberto-orlandi-sonarsource roberto-orlandi-sonarsource force-pushed the rob/SONARARMOR-817 branch 2 times, most recently from adcda81 to 8a06da2 Compare March 24, 2025 09:32
@roberto-orlandi-sonarsource
Copy link
Contributor Author

There is one issue on Next which, according to the way the file is implemented, I am not sure I should fix. Please let me know if I can accept it.

@roberto-orlandi-sonarsource roberto-orlandi-sonarsource marked this pull request as ready for review March 24, 2025 09:54
@roberto-orlandi-sonarsource roberto-orlandi-sonarsource requested a review from a team March 24, 2025 09:56
Copy link
Contributor

@zglicz zglicz left a comment

Choose a reason for hiding this comment

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

Not much to add about the newly added functionality. It follows the same pattern as before.

It would be interesting to revisit how all of this code is generated and maintained. There are many 1000s of repetitive lines that do not contain difficult logic. These comments are outside of the scope of the PR though.

Comment on lines +920 to +934
function visitTSModuleBlock(node: TSESTree.TSModuleBlock) {
return {
body: node.body.map(visitNode),
};
}

function visitTSModuleDeclaration(node: TSESTree.TSModuleDeclaration) {
return {
id: visitNode(node.id),
body: visitNode(node.body),
kind: node.kind,
};
}

Copy link
Contributor

Choose a reason for hiding this comment

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

As I'm reading and looking through this code, there is just so much redundancy and re-inventing the wheel. This seems like it all could be generated or semi-generated. Do you think at some point you would re-think this implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that this could be improved. Do you think it should be us to rethink this implementation? As far as I know, the Web squad owns and is responsible for this part of the code. I may be wrong because I was not part of the effort when this was created. In such a case, we could consider it in the future (but probably not very soon since we have a lot of other priorities at the moment)

Comment on lines +287 to +283
const tSModuleDeclaration = protoMessage.program.body[0].tSModuleDeclaration;
expect(tSModuleDeclaration.kind).toEqual('namespace');
expect(tSModuleDeclaration.id.type).toEqual(NODE_TYPE_ENUM.values['IdentifierType']);
expect(tSModuleDeclaration.id.identifier.name).toEqual('Foo');
expect(tSModuleDeclaration.body.type).toEqual(NODE_TYPE_ENUM.values['TSModuleBlockType']);
expect(tSModuleDeclaration.body.tSModuleBlock.body[0].type).toEqual(
NODE_TYPE_ENUM.values['VariableDeclarationType'],
);
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this tSModuleDeclaration ? is it a class or a plain object? If its an object and you are asserting many properties, would it be more pleasant to compare it like so? With this setup, it's more visible to the reader of the code of the exact nesting and/or structure. However, as I was typing this out, it did feel a bit longish..

Suggested change
const tSModuleDeclaration = protoMessage.program.body[0].tSModuleDeclaration;
expect(tSModuleDeclaration.kind).toEqual('namespace');
expect(tSModuleDeclaration.id.type).toEqual(NODE_TYPE_ENUM.values['IdentifierType']);
expect(tSModuleDeclaration.id.identifier.name).toEqual('Foo');
expect(tSModuleDeclaration.body.type).toEqual(NODE_TYPE_ENUM.values['TSModuleBlockType']);
expect(tSModuleDeclaration.body.tSModuleBlock.body[0].type).toEqual(
NODE_TYPE_ENUM.values['VariableDeclarationType'],
);
expect(protoMessage.program.body[0].tSModuleDeclaration).toStrictEqual({ // or toEqual or toMatchObject
kind: 'namespace',
id: {
type: NODE_TYPE_ENUM.values['IdentifierType'],
identifier: {
name: 'Foo',
}
},
body: {
type: NODE_TYPE_ENUM.values['TSModuleBlockType'],
tSModuleBlock: {
body: [
{
type: NODE_TYPE_ENUM.values['VariableDeclarationType']
}
]
}
}
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While I like the idea you suggest, having this comparison would make this test super-verbose because to satisfy the equality, there are a lot of other fields that need to be added to your suggestion (locations and the well-constructed VariableDeclaration, which has quite a few nested fields as well, for example).
I think that considering this, the current implementation makes more sense. WDYT?

Comment on lines 334 to 336
const serialized = serializeInProtobuf(ast as TSESTree.Program, 'foo.ts');
const deserializedProtoMessage = deserializeProtobuf(serialized);
compareASTs(protoMessage, deserializedProtoMessage);
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the same in the entire test suite? Can this be extracted someplace or at least a single call across all of these tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, I extracted those three lines to a function

Comment on lines 26 to 37
@Test
void test() {
Class<?>[] classes = ESTree.class.getDeclaredClasses();
assertThat(classes).hasSize(113);
assertThat(classes).hasSize(116);

//filter all classes that are interface
var ifaceCount = Arrays.stream(classes).filter(Class::isInterface).count();
assertThat(ifaceCount).isEqualTo(27);
assertThat(ifaceCount).isEqualTo(28);

var recordCount = Arrays.stream(classes).filter(Class::isRecord).count();
assertThat(recordCount).isEqualTo(81);
assertThat(recordCount).isEqualTo(83);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this testing? You add unit tests to verify the business logic and that should be enough.

Should this be removed? It's not adding value. It didn't catch that you are doing anything wrong, just added noise by making you update this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was an existing test. I just adapted the result based on the modifications I did.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe lets remove?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While I agree with not seeing value in those UTs, I would not make removing them part of this PR. I don't feel comfortable removing tests that I did not write, and that I don't know why they were added in the first place.

Comment on lines 39 to 44
@Test
void test_node_subclasses() {
Class<?> sealedClass = ESTree.Node.class;
Class<?>[] permittedSubclasses = sealedClass.getPermittedSubclasses();
assertThat(permittedSubclasses).hasSize(24);
assertThat(permittedSubclasses).hasSize(25);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

same question. Perhaps, I'm not understanding something about the value of these.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here. This test was just adapted.

Comment on lines +838 to +897
@Test
void should_create_ts_module_block() {
Node emptyStatementNode = Node.newBuilder()
.setType(NodeType.EmptyStatementType)
.setEmptyStatement(EmptyStatement.newBuilder().build())
.build();
TSModuleBlock tsModuleBlock = TSModuleBlock.newBuilder().addBody(emptyStatementNode).build();
Node protobufNode = Node.newBuilder()
.setType(NodeType.TSModuleBlockType)
.setTSModuleBlock(tsModuleBlock)
.build();

ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
assertThat(estree).isInstanceOfSatisfying(ESTree.TSModuleBlock.class, block -> {
assertThat(block.body()).hasSize(1);
assertThat(block.body().get(0)).isInstanceOf(ESTree.EmptyStatement.class);
});
}

@Test
void should_create_ts_module_declaration() {
TSModuleDeclaration tsModuleDeclaration = TSModuleDeclaration.newBuilder()
.setId(Node.newBuilder().setType(NodeType.IdentifierType).build())
.setBody(Node.newBuilder().setType(NodeType.TSModuleBlockType).build())
.setKind("module")
.build();

Node protobufNode = Node.newBuilder()
.setType(NodeType.TSModuleDeclarationType)
.setTSModuleDeclaration(tsModuleDeclaration)
.build();

ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
assertThat(estree).isInstanceOfSatisfying(ESTree.TSModuleDeclaration.class, module -> {
assertThat(module.id()).isInstanceOf(ESTree.Identifier.class);
assertThat(module.body()).isPresent();
assertThat(module.body().get()).isInstanceOf(ESTree.TSModuleBlock.class);
assertThat(module.kind()).isEqualTo("module");
});
}

@Test
void should_create_ts_module_declaration_without_body() {
TSModuleDeclaration tsModuleDeclaration = TSModuleDeclaration.newBuilder()
.setId(Node.newBuilder().setType(NodeType.IdentifierType).build())
.setKind("module")
.build();

Node protobufNode = Node.newBuilder()
.setType(NodeType.TSModuleDeclarationType)
.setTSModuleDeclaration(tsModuleDeclaration)
.build();

ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
assertThat(estree).isInstanceOfSatisfying(ESTree.TSModuleDeclaration.class, module -> {
assertThat(module.id()).isInstanceOf(ESTree.Identifier.class);
assertThat(module.body()).isEmpty();
assertThat(module.kind()).isEqualTo("module");
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

wow, this is so verbose. Almost make me think that a simpler, more maintainable way would be to have a generic node, with a field: type (for the TS/JS node name) and props (for the map of properties, which can again be primitives or further nodes) - almost like a json.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very verbose indeed. I went along with the existing UTs style. Finding a more maintainable implementation could be nice in the future.

@roberto-orlandi-sonarsource
Copy link
Contributor Author

Not much to add about the newly added functionality. It follows the same pattern as before.

It would be interesting to revisit how all of this code is generated and maintained. There are many 1000s of repetitive lines that do not contain difficult logic. These comments are outside of the scope of the PR though.

Thanks for the review. As you mentioned, I think your comments are valid but probably out of the scope of this PR (at least the ones regarding bigger refactoring or changing tests not strictly related to this PR). I will merge as soon as the PR is green again, but happy to continue the open discussions.

@roberto-orlandi-sonarsource roberto-orlandi-sonarsource force-pushed the rob/SONARARMOR-817 branch 3 times, most recently from 45aed56 to 75472b3 Compare March 25, 2025 09:38
@zglicz zglicz force-pushed the rob/SONARARMOR-817 branch from 40fccfe to 00301e5 Compare March 25, 2025 11:35
@zglicz zglicz enabled auto-merge (squash) March 25, 2025 11:35
Copy link

Quality Gate failed Quality Gate failed

Failed conditions
66.7% Coverage on New Code (required ≥ 90%)

See analysis details on SonarQube

@zglicz zglicz merged commit fb70e6b into master Mar 25, 2025
15 of 17 checks passed
@zglicz zglicz deleted the rob/SONARARMOR-817 branch March 25, 2025 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants