Skip to content

Commit 72f2e7c

Browse files
authored
fix(jsii): missing deprecation warning in collections (#3756)
When a deprecated member was nested under a collection of values, no deprecation warning was being generated, despite all the necessary information being available. There was a missing case that caused the collection to be validated as if it was the element type, which is obviously incorrect, but thankfully did not cause errors as it typically led to verifying `undefined`, which silently succeeds. Added the necessary code so that collections are deep-validated as they should have been. Fixes #3755 --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 3569018 commit 72f2e7c

File tree

3 files changed

+141
-19
lines changed

3 files changed

+141
-19
lines changed

packages/jsii/jest.config.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
import config from '../../jest.config.mjs';
1+
import { overriddenConfig } from '../../jest.config.mjs';
22

3-
export default config;
3+
export default overriddenConfig({
4+
watchPathIgnorePatterns: ['.*\\.tsx?$'],
5+
});

packages/jsii/lib/transforms/deprecation-warnings.ts

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { symbolIdentifier } from '../symbol-id';
1010
export const WARNINGSCODE_FILE_NAME = '.warnings.jsii.js';
1111
const WARNING_FUNCTION_NAME = 'print';
1212
const PARAMETER_NAME = 'p';
13+
const FOR_LOOP_ITEM_NAME = 'o';
1314
const NAMESPACE = 'jsiiDeprecationWarnings';
1415
const LOCAL_ENUM_NAMESPACE = 'ns';
1516
const VISITED_OBJECTS_SET_NAME = 'visitedObjects';
@@ -236,6 +237,7 @@ function processInterfaceType(
236237
const statement = createTypeHandlerCall(
237238
functionName,
238239
`${PARAMETER_NAME}.${prop.name}`,
240+
prop.type.collection.kind,
239241
);
240242
statementsByProp.set(`${prop.name}_`, statement);
241243
}
@@ -718,25 +720,67 @@ function findType(typeName: string, assemblies: Assembly[]) {
718720
function createTypeHandlerCall(
719721
functionName: string,
720722
parameter: string,
723+
collectionKind?: spec.CollectionKind,
721724
): ts.Statement {
722-
return ts.createIf(
723-
ts.createPrefix(
724-
ts.SyntaxKind.ExclamationToken,
725-
ts.createCall(
726-
ts.createPropertyAccess(
727-
ts.createIdentifier(VISITED_OBJECTS_SET_NAME),
728-
ts.createIdentifier('has'),
725+
switch (collectionKind) {
726+
case spec.CollectionKind.Array:
727+
return ts.createIf(
728+
ts.createBinary(
729+
ts.createIdentifier(parameter),
730+
ts.SyntaxKind.ExclamationEqualsToken,
731+
ts.createNull(),
729732
),
730-
undefined,
731-
[ts.createIdentifier(parameter)],
732-
),
733-
),
734-
ts.createExpressionStatement(
735-
ts.createCall(ts.createIdentifier(functionName), undefined, [
736-
ts.createIdentifier(parameter),
737-
]),
738-
),
739-
);
733+
ts.createForOf(
734+
undefined,
735+
ts.createVariableDeclarationList(
736+
[ts.createVariableDeclaration(FOR_LOOP_ITEM_NAME)],
737+
ts.NodeFlags.Const,
738+
),
739+
ts.createIdentifier(parameter),
740+
createTypeHandlerCall(functionName, FOR_LOOP_ITEM_NAME),
741+
),
742+
);
743+
case spec.CollectionKind.Map:
744+
return ts.createIf(
745+
ts.createBinary(
746+
ts.createIdentifier(parameter),
747+
ts.SyntaxKind.ExclamationEqualsToken,
748+
ts.createNull(),
749+
),
750+
ts.createForOf(
751+
undefined,
752+
ts.createVariableDeclarationList(
753+
[ts.createVariableDeclaration(FOR_LOOP_ITEM_NAME)],
754+
ts.NodeFlags.Const,
755+
),
756+
ts.createCall(
757+
ts.createPropertyAccess(ts.createIdentifier('Object'), 'values'),
758+
undefined,
759+
[ts.createIdentifier(parameter)],
760+
),
761+
createTypeHandlerCall(functionName, FOR_LOOP_ITEM_NAME),
762+
),
763+
);
764+
case undefined:
765+
return ts.createIf(
766+
ts.createPrefix(
767+
ts.SyntaxKind.ExclamationToken,
768+
ts.createCall(
769+
ts.createPropertyAccess(
770+
ts.createIdentifier(VISITED_OBJECTS_SET_NAME),
771+
ts.createIdentifier('has'),
772+
),
773+
undefined,
774+
[ts.createIdentifier(parameter)],
775+
),
776+
),
777+
ts.createExpressionStatement(
778+
ts.createCall(ts.createIdentifier(functionName), undefined, [
779+
ts.createIdentifier(parameter),
780+
]),
781+
),
782+
);
783+
}
740784
}
741785

742786
/**

packages/jsii/test/deprecation-warnings.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,64 @@ function testpkg_Baz(p) {
179179
);
180180
});
181181

182+
test('checks array elements', () => {
183+
const result = compileJsiiForTest(
184+
`
185+
export interface Used { readonly property: boolean; }
186+
export interface Uses { readonly array: Used[]; }
187+
`,
188+
undefined /* callback */,
189+
{ addDeprecationWarnings: true },
190+
);
191+
192+
expect(jsFunction(result, 'testpkg_Uses', '.warnings.jsii'))
193+
.toMatchInlineSnapshot(`
194+
"function testpkg_Uses(p) {
195+
if (p == null)
196+
return;
197+
visitedObjects.add(p);
198+
try {
199+
if (p.array != null)
200+
for (const o of p.array)
201+
if (!visitedObjects.has(o))
202+
testpkg_Used(o);
203+
}
204+
finally {
205+
visitedObjects.delete(p);
206+
}
207+
}"
208+
`);
209+
});
210+
211+
test('checks map elements', () => {
212+
const result = compileJsiiForTest(
213+
`
214+
export interface Used { readonly property: boolean; }
215+
export interface Uses { readonly map: Record<string, Used>; }
216+
`,
217+
undefined /* callback */,
218+
{ addDeprecationWarnings: true },
219+
);
220+
221+
expect(jsFunction(result, 'testpkg_Uses', '.warnings.jsii'))
222+
.toMatchInlineSnapshot(`
223+
"function testpkg_Uses(p) {
224+
if (p == null)
225+
return;
226+
visitedObjects.add(p);
227+
try {
228+
if (p.map != null)
229+
for (const o of Object.values(p.map))
230+
if (!visitedObjects.has(o))
231+
testpkg_Used(o);
232+
}
233+
finally {
234+
visitedObjects.delete(p);
235+
}
236+
}"
237+
`);
238+
});
239+
182240
test('generates exports for all the functions', () => {
183241
const result = compileJsiiForTest(
184242
`
@@ -862,6 +920,24 @@ function jsFile(result: HelperCompilationResult, baseName = 'index'): string {
862920
return file[1];
863921
}
864922

923+
function jsFunction(
924+
result: HelperCompilationResult,
925+
functionName: string,
926+
baseName = 'index',
927+
): string {
928+
const lines = jsFile(result, baseName).split(/\n/);
929+
930+
const startIndex = lines.indexOf(`function ${functionName}(p) {`);
931+
if (startIndex < 0) {
932+
throw new Error(
933+
`Could not find declaration of ${functionName} in file with base name: ${baseName}`,
934+
);
935+
}
936+
const endIndex = lines.indexOf('}', startIndex);
937+
938+
return lines.slice(startIndex, endIndex + 1).join('\n');
939+
}
940+
865941
function createVmContext(compilation: HelperCompilationResult) {
866942
const context = vm.createContext({
867943
exports: {},

0 commit comments

Comments
 (0)