Skip to content

Commit dd66afb

Browse files
authored
feat: add support for "external" stability (#596)
Add support for a new class of stability called "external". This stability class is treated like "stable" for documentation purposes, but always lead to warnings (not errors) for stability comparison purposes.
1 parent 028868d commit dd66afb

File tree

8 files changed

+140
-24
lines changed

8 files changed

+140
-24
lines changed

packages/jsii-diff/lib/stability.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@ export function compareStabilities(original: reflect.Documentable & ApiElement,
2828

2929
function allowedTransitions(start: spec.Stability): spec.Stability[] {
3030
switch (start) {
31-
// Experimental can go to stable or be deprecated
31+
// Experimental can go to stable, external, or be deprecated
3232
case spec.Stability.Experimental:
33-
return [spec.Stability.Stable, spec.Stability.Deprecated];
33+
return [spec.Stability.Stable, spec.Stability.Deprecated, spec.Stability.External];
3434

35-
// Stable can be deprecated
35+
// Stable can be deprecated, or switched to external
3636
case spec.Stability.Stable:
37-
return [spec.Stability.Deprecated];
37+
return [spec.Stability.Deprecated, spec.Stability.External];
3838

3939
// Deprecated can be reinstated
4040
case spec.Stability.Deprecated:
41-
return [spec.Stability.Stable];
41+
return [spec.Stability.Stable, spec.Stability.External];
42+
43+
// external can be stableified, or deprecated
44+
case spec.Stability.External:
45+
return [spec.Stability.Stable, spec.Stability.Deprecated];
4246
}
4347

4448
throw new Error(`Unrecognized stability: ${start}`);

packages/jsii-diff/test/test.diagnostics.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ export = {
2121
test.done();
2222
},
2323

24+
// ----------------------------------------------------------------------
25+
async 'external stability violations are reported as warnings'(test: Test) {
26+
const mms = await compare(`
27+
/** @stability external */
28+
export class Foo1 { }
29+
`, `
30+
export class Foo2 { }
31+
`);
32+
33+
const experimentalErrors = false;
34+
const diags = classifyDiagnostics(mms, experimentalErrors, new Set());
35+
36+
test.equals(1, diags.length);
37+
test.equals(false, hasErrors(diags));
38+
39+
test.done();
40+
},
41+
2442
// ----------------------------------------------------------------------
2543
async 'warnings can be turned into errors'(test: Test) {
2644
const mms = await compare(`
@@ -39,6 +57,24 @@ export = {
3957
test.done();
4058
},
4159

60+
// ----------------------------------------------------------------------
61+
async 'external stability violations are never turned into errors'(test: Test) {
62+
const mms = await compare(`
63+
/** @stability external */
64+
export class Foo1 { }
65+
`, `
66+
export class Foo2 { }
67+
`);
68+
69+
const experimentalErrors = true;
70+
const diags = classifyDiagnostics(mms, experimentalErrors, new Set());
71+
72+
test.equals(1, diags.length);
73+
test.equals(false, hasErrors(diags));
74+
75+
test.done();
76+
},
77+
4278
// ----------------------------------------------------------------------
4379
async 'errors can be skipped'(test: Test) {
4480
const mms = await compare(`

packages/jsii-pacmak/lib/targets/java.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,8 @@ class JavaGenerator extends Generator {
771771
return 'Deprecated';
772772
case spec.Stability.Experimental:
773773
return 'Experimental';
774+
case spec.Stability.External:
775+
return 'External';
774776
case spec.Stability.Stable:
775777
return 'Stable';
776778
}

packages/jsii-reflect/lib/docs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export class Docs {
7070
const stabilityPrecedence = {
7171
[Stability.Deprecated]: 0,
7272
[Stability.Experimental]: 1,
73-
[Stability.Stable]: 2,
73+
[Stability.External]: 2,
74+
[Stability.Stable]: 3,
7475
};
7576

7677
function lowestStability(a?: Stability, b?: Stability): Stability | undefined {

packages/jsii-reflect/test/typesystem.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,30 @@ describe('Stability', () => {
322322
expect(initializer.docs.stability).toEqual(Stability.Experimental);
323323
expect(method.docs.stability).toEqual(Stability.Experimental);
324324
});
325+
326+
test('external stability', async () => {
327+
const ts = await typeSystemFromSource(`
328+
/**
329+
* @stability external
330+
*/
331+
export class Foo {
332+
public foo() {
333+
Array.isArray(3);
334+
}
335+
}
336+
337+
/**
338+
* @stable
339+
*/
340+
export class SubFoo extends Foo {
341+
}
342+
`);
343+
344+
const classType = ts.findClass('testpkg.SubFoo');
345+
const method = classType.allMethods.find(m => m.name === 'foo')!;
346+
347+
expect(method.docs.stability).toEqual(Stability.External);
348+
});
325349
});
326350
});
327351

packages/jsii-spec/lib/spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,12 @@ export enum Stability {
360360
* in breaking ways in a subsequent minor or patch version.
361361
*/
362362
Stable = 'stable',
363+
364+
/**
365+
* This API is an representation of an API managed elsewhere and follows
366+
* the other API's versioning model.
367+
*/
368+
External = 'external',
363369
}
364370

365371
/**

packages/jsii/lib/docs.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,24 @@ function parseDocParts(comments: string | undefined, tags: ts.JSDocTagInfo[]): D
8585
docs.see = eatTag('see');
8686
docs.subclassable = eatTag('subclassable') !== undefined ? true : undefined;
8787

88+
docs.stability = parseStability(eatTag('stability'), diagnostics);
89+
// @experimental is a shorthand for '@stability experimental', same for '@stable'
8890
const experimental = eatTag('experimental') !== undefined;
8991
const stable = eatTag('stable') !== undefined;
92+
// Can't combine them
93+
if (countBools(docs.stability !== undefined, experimental, stable) > 1) {
94+
diagnostics.push(`Use only one of @stability, @experimental or @stable`);
95+
}
96+
if (experimental) { docs.stability = spec.Stability.Experimental; }
97+
if (stable) { docs.stability = spec.Stability.Stable; }
98+
99+
// Can combine '@stability deprecated' with '@deprecated <reason>'
100+
if (docs.deprecated !== undefined) {
101+
if (docs.stability !== undefined && docs.stability !== spec.Stability.Deprecated) {
102+
diagnostics.push(`@deprecated tag requires '@stability deprecated' or no @stability at all.`);
103+
}
104+
docs.stability = spec.Stability.Deprecated;
105+
}
90106

91107
if (docs.example && docs.example.indexOf('```') >= 0) {
92108
// This is currently what the JSDoc standard expects, and VSCode highlights it in
@@ -97,26 +113,10 @@ function parseDocParts(comments: string | undefined, tags: ts.JSDocTagInfo[]): D
97113
diagnostics.push('@example must be code only, no code block fences allowed.');
98114
}
99115

100-
if (experimental && stable) {
101-
diagnostics.push('Element is marked both @experimental and @stable.');
102-
}
103-
104-
if (docs.deprecated !== undefined) {
105-
if (docs.deprecated.trim() === '') {
106-
diagnostics.push('@deprecated tag needs a reason and/or suggested alternatives.');
107-
}
108-
if (stable) {
109-
diagnostics.push('Element is marked both @deprecated and @stable.');
110-
}
111-
if (experimental) {
112-
diagnostics.push('Element is marked both @deprecated and @experimental.');
113-
}
116+
if (docs.deprecated !== undefined && docs.deprecated.trim() === '') {
117+
diagnostics.push('@deprecated tag needs a reason and/or suggested alternatives.');
114118
}
115119

116-
if (experimental) { docs.stability = spec.Stability.Experimental; }
117-
if (stable) { docs.stability = spec.Stability.Stable; }
118-
if (docs.deprecated) { docs.stability = spec.Stability.Deprecated; }
119-
120120
if (tagNames.size > 0) {
121121
docs.custom = {};
122122
for (const [key, value] of tagNames.entries()) {
@@ -184,3 +184,24 @@ function summaryLine(str: string) {
184184
const PUNCTUATION = ['!', '?', '.', ';'].map(s => '\\' + s).join('');
185185
const ENDS_WITH_PUNCTUATION_REGEX = new RegExp(`[${PUNCTUATION}]$`);
186186
const FIRST_SENTENCE_REGEX = new RegExp(`^([^${PUNCTUATION}]+[${PUNCTUATION}] )`); // literal space at the end
187+
188+
function intBool(x: boolean): number {
189+
return x ? 1 : 0;
190+
}
191+
192+
function countBools(...x: boolean[]) {
193+
return x.map(intBool).reduce((a, b) => a + b, 0);
194+
}
195+
196+
function parseStability(s: string | undefined, diagnostics: string[]): spec.Stability | undefined {
197+
if (s === undefined) { return undefined; }
198+
199+
switch (s) {
200+
case 'stable': return spec.Stability.Stable;
201+
case 'experimental': return spec.Stability.Experimental;
202+
case 'external': return spec.Stability.External;
203+
case 'deprecated': return spec.Stability.Deprecated;
204+
}
205+
diagnostics.push(`Unrecognized @stability: '${s}'`);
206+
return undefined;
207+
}

packages/jsii/test/test.docs.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,28 @@ export = {
269269
test.done();
270270
},
271271

272+
// ----------------------------------------------------------------------
273+
274+
async 'can mark external'(test: Test) {
275+
const assembly = await compile(`
276+
/**
277+
* @stability external
278+
*/
279+
export class Foo {
280+
public floop() {
281+
Array.isArray(3);
282+
}
283+
}
284+
`);
285+
286+
const classType = assembly.types!['testpkg.Foo'] as spec.ClassType;
287+
const method = classType.methods!.find(m => m.name === 'floop');
288+
289+
test.deepEqual(classType.docs!.stability, spec.Stability.External);
290+
test.deepEqual(method!.docs!.stability, spec.Stability.External);
291+
test.done();
292+
},
293+
272294
// ----------------------------------------------------------------------
273295
async 'can mark subclassable'(test: Test) {
274296
const assembly = await compile(`

0 commit comments

Comments
 (0)