Parenthesize TS unions correctly. Fixes #523

This commit is contained in:
Mark Probst 2018-02-15 07:35:41 -08:00
Родитель f4f903e982
Коммит 260d484bef
4 изменённых файлов: 69 добавлений и 60 удалений

Просмотреть файл

@ -764,7 +764,7 @@ export abstract class ConvenienceRenderer extends Renderer {
protected registerHandlebarsHelpers(context: StringMap): void {
super.registerHandlebarsHelpers(context);
handlebars.registerHelper("with_type", function(t: any, options: any): any {
handlebars.registerHelper("with_type", function (t: any, options: any): any {
return options.fn(context.allTypes[t.index]);
});
}

Просмотреть файл

@ -22,7 +22,7 @@ import {
allUpperWordStyle
} from "../Strings";
import { defined, intercalate } from "../Support";
import { Sourcelike, annotated } from "../Source";
import { Sourcelike, annotated, MultiWord, singleWord, multiWord, parenIfNeeded } from "../Source";
import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation";
export default class ElmTargetLanguage extends TargetLanguage {
@ -116,26 +116,6 @@ function elmNameStyle(original: string, upper: boolean): string {
const upperNamingFunction = funPrefixNamer("upper", n => elmNameStyle(n, true));
const lowerNamingFunction = funPrefixNamer("lower", n => elmNameStyle(n, false));
type MultiWord = {
source: Sourcelike;
needsParens: boolean;
};
function singleWord(source: Sourcelike): MultiWord {
return { source, needsParens: false };
}
function multiWord(a: Sourcelike, b: Sourcelike): MultiWord {
return { source: [a, " ", b], needsParens: true };
}
function parenIfNeeded({ source, needsParens }: MultiWord): Sourcelike {
if (needsParens) {
return ["(", source, ")"];
}
return source;
}
type RequiredOrOptional = {
reqOrOpt: string;
fallback: string;
@ -258,16 +238,16 @@ class ElmRenderer extends ConvenienceRenderer {
_integerType => singleWord("Int"),
_doubleType => singleWord("Float"),
_stringType => singleWord("String"),
arrayType => multiWord(this.arrayType, parenIfNeeded(this.elmType(arrayType.items))),
arrayType => multiWord(" ", this.arrayType, parenIfNeeded(this.elmType(arrayType.items))),
classType => singleWord(this.nameForNamedType(classType)),
mapType => multiWord("Dict String", parenIfNeeded(this.elmType(mapType.values))),
mapType => multiWord(" ", "Dict String", parenIfNeeded(this.elmType(mapType.values))),
enumType => singleWord(this.nameForNamedType(enumType)),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable !== null) {
const nullableType = this.elmType(nullable);
if (noOptional) return nullableType;
return multiWord("Maybe", parenIfNeeded(nullableType));
return multiWord(" ", "Maybe", parenIfNeeded(nullableType));
}
return singleWord(this.nameForNamedType(unionType));
}
@ -276,7 +256,7 @@ class ElmRenderer extends ConvenienceRenderer {
private elmProperty(p: ClassProperty): Sourcelike {
if (p.isOptional) {
return multiWord("Maybe", parenIfNeeded(this.elmType(p.type, true))).source;
return multiWord(" ", "Maybe", parenIfNeeded(this.elmType(p.type, true))).source;
} else {
return this.elmType(p.type).source;
}
@ -291,25 +271,25 @@ class ElmRenderer extends ConvenienceRenderer {
return matchType<MultiWord>(
t,
_anyType => singleWord("Jdec.value"),
_nullType => multiWord("Jdec.null", "()"),
_nullType => multiWord(" ", "Jdec.null", "()"),
_boolType => singleWord("Jdec.bool"),
_integerType => singleWord("Jdec.int"),
_doubleType => singleWord("Jdec.float"),
_stringType => singleWord("Jdec.string"),
arrayType =>
multiWord(
multiWord(" ",
["Jdec.", decapitalize(this.arrayType)],
parenIfNeeded(this.decoderNameForType(arrayType.items))
),
classType => singleWord(this.decoderNameForNamedType(classType)),
mapType => multiWord("Jdec.dict", parenIfNeeded(this.decoderNameForType(mapType.values))),
mapType => multiWord(" ", "Jdec.dict", parenIfNeeded(this.decoderNameForType(mapType.values))),
enumType => singleWord(this.decoderNameForNamedType(enumType)),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable !== null) {
const nullableDecoder = this.decoderNameForType(nullable);
if (noOptional) return nullableDecoder;
return multiWord("Jdec.nullable", parenIfNeeded(nullableDecoder));
return multiWord(" ", "Jdec.nullable", parenIfNeeded(nullableDecoder));
}
return singleWord(this.decoderNameForNamedType(unionType));
}
@ -318,7 +298,7 @@ class ElmRenderer extends ConvenienceRenderer {
private decoderNameForProperty(p: ClassProperty): MultiWord {
if (p.isOptional) {
return multiWord("Jdec.nullable", parenIfNeeded(this.decoderNameForType(p.type, true)));
return multiWord(" ", "Jdec.nullable", parenIfNeeded(this.decoderNameForType(p.type, true)));
} else {
return this.decoderNameForType(p.type);
}
@ -333,22 +313,22 @@ class ElmRenderer extends ConvenienceRenderer {
return matchType<MultiWord>(
t,
_anyType => singleWord("identity"),
_nullType => multiWord("always", "Jenc.null"),
_nullType => multiWord(" ", "always", "Jenc.null"),
_boolType => singleWord("Jenc.bool"),
_integerType => singleWord("Jenc.int"),
_doubleType => singleWord("Jenc.float"),
_stringType => singleWord("Jenc.string"),
arrayType =>
multiWord(["make", this.arrayType, "Encoder"], parenIfNeeded(this.encoderNameForType(arrayType.items))),
multiWord(" ", ["make", this.arrayType, "Encoder"], parenIfNeeded(this.encoderNameForType(arrayType.items))),
classType => singleWord(this.encoderNameForNamedType(classType)),
mapType => multiWord("makeDictEncoder", parenIfNeeded(this.encoderNameForType(mapType.values))),
mapType => multiWord(" ", "makeDictEncoder", parenIfNeeded(this.encoderNameForType(mapType.values))),
enumType => singleWord(this.encoderNameForNamedType(enumType)),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable !== null) {
const nullableEncoder = this.encoderNameForType(nullable);
if (noOptional) return nullableEncoder;
return multiWord("makeNullableEncoder", parenIfNeeded(nullableEncoder));
return multiWord(" ", "makeNullableEncoder", parenIfNeeded(nullableEncoder));
}
return singleWord(this.encoderNameForNamedType(unionType));
}
@ -357,7 +337,7 @@ class ElmRenderer extends ConvenienceRenderer {
private encoderNameForProperty(p: ClassProperty): MultiWord {
if (p.isOptional) {
return multiWord("makeNullableEncoder", parenIfNeeded(this.encoderNameForType(p.type, true)));
return multiWord(" ", "makeNullableEncoder", parenIfNeeded(this.encoderNameForType(p.type, true)));
} else {
return this.encoderNameForType(p.type);
}

Просмотреть файл

@ -24,7 +24,7 @@ import {
} from "../Strings";
import { intercalate } from "../Support";
import { Sourcelike, modifySource } from "../Source";
import { Sourcelike, modifySource, MultiWord, singleWord, parenIfNeeded, multiWord } from "../Source";
import { Namer, Name } from "../Naming";
import { ConvenienceRenderer } from "../ConvenienceRenderer";
import { TargetLanguage } from "../TargetLanguage";
@ -147,39 +147,39 @@ class TypeScriptRenderer extends ConvenienceRenderer {
});
};
sourceFor = (t: Type): Sourcelike => {
return matchType<Sourcelike>(
private sourceFor(t: Type): MultiWord {
return matchType<MultiWord>(
t,
_anyType => "any",
_nullType => "null",
_boolType => "boolean",
_integerType => "number",
_doubleType => "number",
_stringType => "string",
_anyType => singleWord("any"),
_nullType => singleWord("null"),
_boolType => singleWord("boolean"),
_integerType => singleWord("number"),
_doubleType => singleWord("number"),
_stringType => singleWord("string"),
arrayType => {
const itemType = this.sourceFor(arrayType.items);
if (
(arrayType.items instanceof UnionType && this._inlineUnions) ||
arrayType.items instanceof ArrayType
) {
return ["Array<", itemType, ">"];
return singleWord(["Array<", itemType.source, ">"]);
} else {
return [itemType, "[]"];
return singleWord([parenIfNeeded(itemType), "[]"]);
}
},
classType => this.nameForNamedType(classType),
mapType => ["{ [key: string]: ", this.sourceFor(mapType.values), " }"],
enumType => this.nameForNamedType(enumType),
classType => singleWord(this.nameForNamedType(classType)),
mapType => singleWord(["{ [key: string]: ", this.sourceFor(mapType.values).source, " }"]),
enumType => singleWord(this.nameForNamedType(enumType)),
unionType => {
if (this._inlineUnions || nullableFromUnion(unionType) !== null) {
const children = unionType.children.map(this.sourceFor);
return intercalate(" | ", children).toArray();
const children = unionType.children.map(c => parenIfNeeded(this.sourceFor(c)));
return multiWord(" | ", ...children.toArray());
} else {
return this.nameForNamedType(unionType);
return singleWord(this.nameForNamedType(unionType));
}
}
);
};
}
typeMapTypeFor = (t: Type): Sourcelike => {
return matchType<Sourcelike>(
@ -246,7 +246,7 @@ class TypeScriptRenderer extends ConvenienceRenderer {
}
table.push([
[name, nullable !== null ? "?" : "", ": "],
[this.sourceFor(nullable !== null ? nullable : t), ";"]
[this.sourceFor(nullable !== null ? nullable : t).source, ";"]
]);
});
this.emitTable(table);
@ -260,7 +260,7 @@ class TypeScriptRenderer extends ConvenienceRenderer {
}
this.emitBlock("export module Convert", "", () => {
this.forEachTopLevel("interposing", (t, name) => {
this.emitBlock(["export function to", name, "(json: string): ", this.sourceFor(t)], "", () => {
this.emitBlock(["export function to", name, "(json: string): ", this.sourceFor(t).source], "", () => {
if (this._omitRuntimeTypecheck) {
this.emitLine("return JSON.parse(json);");
} else {
@ -271,7 +271,7 @@ class TypeScriptRenderer extends ConvenienceRenderer {
const camelCaseName = modifySource(camelCase, name);
this.emitBlock(
["export function ", camelCaseName, "ToJson(value: ", this.sourceFor(t), "): string"],
["export function ", camelCaseName, "ToJson(value: ", this.sourceFor(t).source, "): string"],
"",
() => {
this.emitLine("return JSON.stringify(value, null, 2);");
@ -366,8 +366,8 @@ function O(className: string) {
if (this._inlineUnions) {
return;
}
const children = u.children.map(this.sourceFor);
this.emitLine("export type ", unionName, " = ", intercalate(" | ", children).toArray(), ";");
const children = multiWord(" | ", ...u.children.map(c => parenIfNeeded(this.sourceFor(c))).toArray());
this.emitLine("export type ", unionName, " = ", children.source, ";");
};
protected emitSourceStructure() {

Просмотреть файл

@ -66,7 +66,7 @@ export function newline(): NewlineSource {
}
export type Sourcelike = Source | string | Name | SourcelikeArray;
export interface SourcelikeArray extends Array<Sourcelike> {}
export interface SourcelikeArray extends Array<Sourcelike> { }
export function sourcelikeToSource(sl: Sourcelike): Source {
if (sl instanceof Array) {
@ -263,3 +263,32 @@ export function serializeRenderResult(
finishLine();
return { lines, annotations: List(annotations) };
}
export type MultiWord = {
source: Sourcelike;
needsParens: boolean;
};
export function singleWord(source: Sourcelike): MultiWord {
return { source, needsParens: false };
}
export function multiWord(separator: Sourcelike, ...words: Sourcelike[]): MultiWord {
assert(words.length > 0, "Zero words is not multiple");
if (words.length === 1) {
return singleWord(words[0]);
}
const items: Sourcelike[] = [];
for (let i = 0; i < words.length; i++) {
if (i > 0) items.push(separator);
items.push(words[i]);
}
return { source: items, needsParens: true };
}
export function parenIfNeeded({ source, needsParens }: MultiWord): Sourcelike {
if (needsParens) {
return ["(", source, ")"];
}
return source;
}