Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions lib/codegen/fromcto/typescript/typescriptvisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ class TypescriptVisitor {
} else if (thing.isMapDeclaration?.()) {
return this.visitMapDeclaration(thing, parameters);
} else if (thing.isTypeScalar?.()) {
return this.visitField(thing.getScalarField(), parameters);
// Propagate optional modifier from the field to visitField via parameters
// Avoid mutating the shared scalar descriptor
const scalarField = thing.getScalarField();
return this.visitField(scalarField, { ...parameters, forceOptional: thing.isOptional() });
} else if (thing.isField?.()) {
return this.visitField(thing, parameters);
} else if (thing.isRelationship?.()) {
Expand Down Expand Up @@ -247,7 +250,7 @@ class TypescriptVisitor {
array = '[]';
}

if (field.isOptional()) {
if (field.isOptional() || parameters.forceOptional) {
optional = '?';
}
const isEnumRef = field.isPrimitive() ? false
Expand Down
33 changes: 33 additions & 0 deletions test/codegen/fromcto/typescript/typescriptvisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,39 @@ describe('TypescriptVisitor', function () {
mockSpecialVisit.calledWith(thing, param).should.be.ok;
});

it('should propagate optional modifier for scalar fields via parameters', () => {
// Create a mock scalar field
let mockScalarField = sinon.createStubInstance(Field);
mockScalarField.isOptional.returns(false); // Scalar field itself is not optional

// Create a thing that is a scalar type field
let thing = {
isModelManager: () => false,
isModelFile: () => false,
isEnum: () => false,
isClassDeclaration: () => false,
isMapDeclaration: () => false,
isTypeScalar: () => true,
isField: () => true,
isRelationship: () => false,
isEnumValue: () => false,
isOptional: () => true, // The parent field is optional
getScalarField: () => mockScalarField
};

let mockSpecialVisit = sinon.stub(typescriptVisitor, 'visitField');
mockSpecialVisit.returns('Duck');

typescriptVisitor.visit(thing, param);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

This test does not assert the return value of typescriptVisitor.visit(thing, param), unlike every other test in the visit describe block, all of which assert .should.deep.equal('Duck'). The return value of the scalar path should also be verified.

Suggested change
typescriptVisitor.visit(thing, param);
const result = typescriptVisitor.visit(thing, param);
result.should.deep.equal('Duck');

Copilot uses AI. Check for mistakes.

// Verify that visitField was called with the scalar field
mockSpecialVisit.calledOnce.should.be.ok;
// Verify that forceOptional was passed via parameters (not by mutating the scalar field)
const callArgs = mockSpecialVisit.getCall(0).args;
callArgs[0].should.equal(mockScalarField);
callArgs[1].forceOptional.should.equal(true);
Comment on lines +158 to +168
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The new test verifies that visitField is called with forceOptional: true in the parameters, but it does not verify the actual rendering output. The test stubs out visitField entirely, so it never exercises the updated condition field.isOptional() || parameters.forceOptional at line 253 of the source file. A test in the visitField describe block that passes { forceOptional: true } as parameters (with a field where isOptional() returns false) and asserts that the output contains ? would provide meaningful end-to-end coverage of the fix.

Suggested change
let mockSpecialVisit = sinon.stub(typescriptVisitor, 'visitField');
mockSpecialVisit.returns('Duck');
typescriptVisitor.visit(thing, param);
// Verify that visitField was called with the scalar field
mockSpecialVisit.calledOnce.should.be.ok;
// Verify that forceOptional was passed via parameters (not by mutating the scalar field)
const callArgs = mockSpecialVisit.getCall(0).args;
callArgs[0].should.equal(mockScalarField);
callArgs[1].forceOptional.should.equal(true);
// Ensure the visitor has a FileWriter so that visitField can render output
param.fileWriter = mockFileWriter;
// Spy on visitField to exercise the real implementation while inspecting parameters
let visitFieldSpy = sinon.spy(typescriptVisitor, 'visitField');
typescriptVisitor.visit(thing, param);
// Verify that visitField was called with the scalar field
visitFieldSpy.calledOnce.should.be.ok;
// Verify that forceOptional was passed via parameters (not by mutating the scalar field)
const callArgs = visitFieldSpy.getCall(0).args;
callArgs[0].should.equal(mockScalarField);
callArgs[1].forceOptional.should.equal(true);
// Verify that the rendered output reflects the optional modifier (contains '?')
mockFileWriter.writeLine.called.should.be.ok;
const renderedLine = mockFileWriter.writeLine.getCall(0).args[0];
renderedLine.should.contain('?');

Copilot uses AI. Check for mistakes.
});

it('should throw an error when an unrecognised type is supplied', () => {
let thing = 'Something of unrecognised type';

Expand Down
Loading