Skip to content

Commit ca44537

Browse files
RomainMullermergify[bot]
authored andcommitted
feat(jsii): emit warnings when using reserved words (#704)
* feat(jsii): emit warnings when using reserved words Emits warnings when an exported API is named after a reserved word in any of the supported target languages (best-effort). This warns users that their code may have toruble compiling for certain target languages and invites them to use a different name. Additionally, a `--fail-on-warnings` / `--Werr` option was added to the CLI that allows treating warnings as errors, allowing users to create and maintain a codebase that does not rely on naming things in ways that will cause conflicts in target languages. Fixes #701 * also prohibit using 'build'
1 parent f3d1da0 commit ca44537

8 files changed

Lines changed: 259 additions & 27 deletions

File tree

packages/jsii/bin/jsii.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import { VERSION } from '../lib/version';
2222
default: true,
2323
desc: 'Automatically add missing entries in the peerDependencies section of package.json'
2424
})
25+
.options('fail-on-warnings', {
26+
alias: 'Werr',
27+
type: 'boolean',
28+
desc: 'Treat warnings as errors'
29+
})
2530
.help()
2631
.version(VERSION)
2732
.argv;
@@ -35,15 +40,16 @@ import { VERSION } from '../lib/version';
3540
const compiler = new Compiler({
3641
projectInfo,
3742
watch: argv.watch,
38-
projectReferences: argv['project-references']
43+
projectReferences: argv['project-references'],
44+
failOnWarnings: argv['fail-on-warnings']
3945
});
4046

4147
return { projectRoot, emitResult: await compiler.emit() };
4248
})().then(({ projectRoot, emitResult }) => {
4349
for (const diagnostic of emitResult.diagnostics) {
4450
utils.logDiagnostic(diagnostic, projectRoot);
4551
}
46-
if (emitResult.hasErrors) {
52+
if (emitResult.emitSkipped) {
4753
process.exit(1);
4854
}
4955
}).catch(e => {

packages/jsii/lib/assembler.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getReferencedDocParams, parseSymbolDocumentation } from './docs';
1212
import { Diagnostic, EmitResult, Emitter } from './emitter';
1313
import literate = require('./literate');
1414
import { ProjectInfo } from './project-info';
15+
import { isReservedName } from './reserved-words';
1516
import { Validator } from './validator';
1617
import { SHORT_VERSION, VERSION } from './version';
1718

@@ -91,7 +92,7 @@ export class Assembler implements Emitter {
9192
// Clearing ``this._types`` to allow contents to be garbage-collected.
9293
delete this._types;
9394
try {
94-
return { diagnostics: this._diagnostics, hasErrors: true };
95+
return { diagnostics: this._diagnostics, emitSkipped: true };
9596
} finally {
9697
// Clearing ``this._diagnostics`` to allow contents to be garbage-collected.
9798
delete this._diagnostics;
@@ -124,7 +125,7 @@ export class Assembler implements Emitter {
124125

125126
const validator = new Validator(this.projectInfo, assembly);
126127
const validationResult = await validator.emit();
127-
if (!validationResult.hasErrors) {
128+
if (!validationResult.emitSkipped) {
128129
const assemblyPath = path.join(this.projectInfo.projectRoot, '.jsii');
129130
LOG.trace(`Emitting assembly: ${colors.blue(assemblyPath)}`);
130131
await fs.writeJson(assemblyPath, _fingerprint(assembly), { encoding: 'utf8', spaces: 2 });
@@ -133,7 +134,7 @@ export class Assembler implements Emitter {
133134
try {
134135
return {
135136
diagnostics: [...this._diagnostics, ...validationResult.diagnostics],
136-
hasErrors: validationResult.hasErrors
137+
emitSkipped: validationResult.emitSkipped
137138
};
138139
} finally {
139140
// Clearing ``this._types`` to allow contents to be garbage-collected.
@@ -439,6 +440,8 @@ export class Assembler implements Emitter {
439440
return undefined;
440441
}
441442

443+
this._warnAboutReservedWords(type.symbol);
444+
442445
const fqn = `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${type.symbol.name}`;
443446

444447
const jsiiType: spec.ClassType = {
@@ -747,6 +750,8 @@ export class Assembler implements Emitter {
747750
return undefined;
748751
}
749752

753+
this._warnAboutReservedWords(type.symbol);
754+
750755
const decl = symbol.valueDeclaration;
751756
const flags = ts.getCombinedModifierFlags(decl);
752757
// tslint:disable-next-line:no-bitwise
@@ -822,6 +827,8 @@ export class Assembler implements Emitter {
822827
return undefined;
823828
}
824829

830+
this._warnAboutReservedWords(type.symbol);
831+
825832
const fqn = `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${type.symbol.name}`;
826833

827834
const jsiiType: spec.InterfaceType = {
@@ -954,6 +961,8 @@ export class Assembler implements Emitter {
954961
this._diagnostic(declaration, ts.DiagnosticCategory.Error, `Prohibited member name: ${symbol.name}`);
955962
return;
956963
}
964+
this._warnAboutReservedWords(symbol);
965+
957966
const parameters = await Promise.all(signature.getParameters().map(p => this._toParameter(p, ctx)));
958967

959968
const returnType = signature.getReturnType();
@@ -1006,6 +1015,16 @@ export class Assembler implements Emitter {
10061015
type.methods.push(method);
10071016
}
10081017

1018+
private _warnAboutReservedWords(symbol: ts.Symbol) {
1019+
const reservingLanguages = isReservedName(symbol.name);
1020+
if (reservingLanguages) {
1021+
this._diagnostic(symbol.valueDeclaration,
1022+
ts.DiagnosticCategory.Warning,
1023+
`'${symbol.name}' is a reserved word in ${reservingLanguages.join(', ')}. Using this name may cause problems `
1024+
+ 'when generating language bindings. Consider using a different name.');
1025+
}
1026+
}
1027+
10091028
private async _visitProperty(symbol: ts.Symbol, type: spec.ClassType | spec.InterfaceType, ctx: EmitContext) {
10101029
if (type.properties && type.properties.find(p => p.name === symbol.name)) {
10111030
/*
@@ -1024,6 +1043,8 @@ export class Assembler implements Emitter {
10241043
return;
10251044
}
10261045

1046+
this._warnAboutReservedWords(symbol);
1047+
10271048
const signature = symbol.valueDeclaration as (ts.PropertySignature
10281049
| ts.PropertyDeclaration
10291050
| ts.AccessorDeclaration
@@ -1069,6 +1090,8 @@ export class Assembler implements Emitter {
10691090
}
10701091
const paramDeclaration = paramSymbol.valueDeclaration as ts.ParameterDeclaration;
10711092

1093+
this._warnAboutReservedWords(paramSymbol);
1094+
10721095
const parameter: spec.Parameter = {
10731096
...await this._optionalValue(this._typeChecker.getTypeAtLocation(paramSymbol.valueDeclaration), paramSymbol.valueDeclaration),
10741097
name: paramSymbol.name,
@@ -1643,12 +1666,11 @@ function isErrorType(t: ts.Type) {
16431666
}
16441667

16451668
/**
1646-
* These specifially cause trouble in C#, where we have to specificially annotate them as 'new' but our generator isn't doing that
1647-
*
1648-
* In C#, 'GetHashCode' is also problematic, but jsii already prevents you from naming a
1649-
* method that starts with 'get' so we don't need to do anything special for that.
1669+
* Those have specific semantics in certain languages that don't always translate cleanly in others
1670+
* (like how equals/hashCode are not a thing in Javascript, but carry meaning in Java and C#). The
1671+
* `build` name is reserved for generated code (Java builders use that).
16501672
*/
1651-
const PROHIBITED_MEMBER_NAMES = ['equals', 'hashcode'];
1673+
const PROHIBITED_MEMBER_NAMES = ['build', 'equals', 'hashcode'];
16521674

16531675
/**
16541676
* Whether the given name is prohibited

packages/jsii/lib/compiler.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface CompilerOptions {
4444
watch?: boolean;
4545
/** Whether to detect and generate TypeScript project references */
4646
projectReferences?: boolean;
47+
/** Whether to fail when a warning is emitted */
48+
failOnWarnings?: boolean;
4749
}
4850

4951
export interface TypescriptConfig {
@@ -147,7 +149,7 @@ export class Compiler implements Emitter {
147149

148150
private async _consumeProgram(program: ts.Program, stdlib: string): Promise<EmitResult> {
149151
const emit = program.emit();
150-
let hasErrors = emitHasErrors(emit);
152+
let hasErrors = emitHasErrors(emit, this.options.failOnWarnings);
151153
const diagnostics = [...emit.diagnostics];
152154

153155
if (hasErrors) {
@@ -160,17 +162,17 @@ export class Compiler implements Emitter {
160162
try {
161163
const assembler = new Assembler(this.options.projectInfo, program, stdlib);
162164
const assmEmit = await assembler.emit();
163-
if (assmEmit.hasErrors) {
165+
if (assmEmit.emitSkipped) {
164166
LOG.error('Type model errors prevented the JSII assembly from being created');
165167
}
166168

167-
hasErrors = hasErrors || assmEmit.hasErrors;
169+
hasErrors = hasErrors || emitHasErrors(assmEmit, this.options.failOnWarnings);
168170
diagnostics.push(...assmEmit.diagnostics);
169171
} catch (e) {
170172
LOG.error(`Error during type model analysis: ${e}`);
171173
}
172174

173-
return { hasErrors, diagnostics };
175+
return { emitSkipped: hasErrors, diagnostics, emittedFiles: emit.emittedFiles };
174176
}
175177

176178
/**
@@ -371,6 +373,9 @@ function parseConfigHostFromCompilerHost(host: ts.CompilerHost): ts.ParseConfigH
371373
};
372374
}
373375

374-
function emitHasErrors(result: ts.EmitResult) {
375-
return result.diagnostics.some(d => d.category === ts.DiagnosticCategory.Error) || result.emitSkipped;
376+
function emitHasErrors(result: ts.EmitResult, includeWarnings?: boolean) {
377+
return result.diagnostics.some(d =>
378+
d.category === ts.DiagnosticCategory.Error
379+
|| (includeWarnings && d.category === ts.DiagnosticCategory.Warning)
380+
) || result.emitSkipped;
376381
}

packages/jsii/lib/emitter.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ export interface Emitter {
1515
/**
1616
* The result of attempting to emit stuff.
1717
*/
18-
export interface EmitResult {
19-
/** Whether the emit was skipped as a result of errors (found in ``diagnostics``) */
20-
hasErrors: boolean;
21-
18+
export interface EmitResult extends ts.EmitResult {
2219
/** Diagnostic information created when trying to emit stuff */
2320
diagnostics: ReadonlyArray<ts.Diagnostic | Diagnostic>;
2421
}

0 commit comments

Comments
 (0)