Skip to content

Const enums #970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Nov 3, 2014
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0d171ca
initial implementation of constant folding
vladima Oct 26, 2014
97460f5
handle non-qualified names, add 'propagateEnumConstants' command line…
vladima Oct 26, 2014
ce336bc
added folding for references to enum members in enum member initializ…
vladima Oct 26, 2014
365587f
addressed CR feedback, added support for indexed access
vladima Oct 27, 2014
cb472eb
move code around to consolidate checks in one place
vladima Oct 27, 2014
03cb645
dropped redundand type assertion, added mising check
vladima Oct 27, 2014
329d6e2
merge with master
vladima Oct 28, 2014
2dd9511
'const enum' iteration 0. TODO: allow and track const enums in import…
vladima Oct 28, 2014
6f4ea86
merge with master
vladima Oct 29, 2014
e949eda
const enums, iteration 1: const enums can be used in imports, const e…
vladima Oct 29, 2014
4aa4ea7
allow arithmetic operations in constant expressions, handle infinity\…
vladima Oct 30, 2014
270d187
addressed CR feedback
vladima Oct 30, 2014
dd57c6c
added .d.ts generation tests
vladima Oct 31, 2014
ac54fbf
set 'earlyError' bit to 'non-constant expression in constant enum ini…
vladima Oct 31, 2014
7d80b71
do not treat module that contains only const enums as instantiated
vladima Nov 1, 2014
8662c68
add test for 'preserveConstEnums' command line argument
vladima Nov 1, 2014
0b738e8
merge with master
vladima Nov 1, 2014
2d94030
inline enum constant values for indexed access when index is string l…
vladima Nov 2, 2014
4d354c0
addressed CR feedback: adjusted text of error messages, added descrip…
vladima Nov 3, 2014
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
98 changes: 75 additions & 23 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,12 @@ module ts {
}

// Resolves a qualified name and any involved import aliases
function resolveEntityName(location: Node, name: EntityName, meaning: SymbolFlags): Symbol {
function resolveEntityName(location: Node, name: EntityName, meaning: SymbolFlags, suppressErrors?: boolean): Symbol {
if (name.kind === SyntaxKind.Identifier) {
// TODO: Investigate error recovery for symbols not found
var symbol = resolveName(location, (<Identifier>name).text, meaning, Diagnostics.Cannot_find_name_0, identifierToString(<Identifier>name));
var nameNotFoundMessage = !suppressErrors && Diagnostics.Cannot_find_name_0;
var nameArg = !suppressErrors && identifierToString(<Identifier>name);
var symbol = resolveName(location, (<Identifier>name).text, meaning, nameNotFoundMessage, nameArg);
if (!symbol) {
return;
}
Expand All @@ -502,8 +504,10 @@ module ts {
if (!namespace || namespace === unknownSymbol || (<QualifiedName>name).right.kind === SyntaxKind.Missing) return;
var symbol = getSymbol(namespace.exports, (<QualifiedName>name).right.text, meaning);
if (!symbol) {
error(location, Diagnostics.Module_0_has_no_exported_member_1, getFullyQualifiedName(namespace),
identifierToString((<QualifiedName>name).right));
if (!suppressErrors) {
error(location, Diagnostics.Module_0_has_no_exported_member_1, getFullyQualifiedName(namespace),
identifierToString((<QualifiedName>name).right));
}
return;
}
}
Expand Down Expand Up @@ -7397,23 +7401,6 @@ module ts {
}
}

function getConstantValueForExpression(node: Expression): number {
var isNegative = false;
if (node.kind === SyntaxKind.PrefixOperator) {
var unaryExpression = <UnaryExpression>node;
if (unaryExpression.operator === SyntaxKind.MinusToken || unaryExpression.operator === SyntaxKind.PlusToken) {
node = unaryExpression.operand;
isNegative = unaryExpression.operator === SyntaxKind.MinusToken;
}
}
if (node.kind === SyntaxKind.NumericLiteral) {
var literalText = (<LiteralExpression>node).text;
return isNegative ? -literalText : +literalText;
}

return undefined;
}

function computeEnumMemberValues(node: EnumDeclaration) {
var nodeLinks = getNodeLinks(node);

Expand All @@ -7423,13 +7410,13 @@ module ts {
var autoValue = 0;
var ambient = isInAmbientContext(node);

forEach(node.members, member => {
forEach(node.members, member => {
Copy link
Contributor

Choose a reason for hiding this comment

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

errant space.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if(isNumericName(member.name.text)) {
error(member.name, Diagnostics.An_enum_member_cannot_have_a_numeric_name);
}
var initializer = member.initializer;
if (initializer) {
autoValue = getConstantValueForExpression(initializer);
autoValue = getConstantValueForEnumMemberInitializer(member, initializer);
Copy link
Contributor

Choose a reason for hiding this comment

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

passing member and initializer seems redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if (autoValue === undefined && !ambient) {
// Only here do we need to check that the initializer is assignable to the enum type.
// If it is a constant value (not undefined), it is syntactically constrained to be a number.
Expand All @@ -7449,6 +7436,71 @@ module ts {

nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed;
}

function getConstantValueForEnumMemberInitializer(member: EnumMember, initializer: Expression): number {
return evalConstant(initializer);

function evalConstant(e: Node): number {
Copy link
Member

Choose a reason for hiding this comment

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

Seems like all you're getting from introducing a helper function is a nicer name - it's a little unnecessary, but I don't mind it all that much.

switch (e.kind) {
case SyntaxKind.PrefixOperator:
var value = evalConstant((<UnaryExpression>e).operand);
if (value === undefined) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

curlies+newline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

switch ((<UnaryExpression>e).operator) {
case SyntaxKind.PlusToken: return value;
Copy link
Contributor

Choose a reason for hiding this comment

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

these are new capabilities not previously in the compiler. do we intend to put these behind a flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not really, we used to handle UnaryExpression with '+' and '-' as operators. I'll cover '~' by the flag

case SyntaxKind.MinusToken: return -value;
Copy link
Contributor

Choose a reason for hiding this comment

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

note: the spec (at one point) specified that this was a signed-integer. If so, then there can be no space between the - and the value. Also, things like - - 10 are not allowed.

case SyntaxKind.TildeToken: return ~value;
}
return undefined;
case SyntaxKind.BinaryExpression:
if (!program.getCompilerOptions().propagateEnumConstants) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

curlies.


var left = evalConstant((<BinaryExpression>e).left);
if (left === undefined) return undefined;
var right = evalConstant((<BinaryExpression>e).right);
if (right === undefined) return undefined;
switch ((<BinaryExpression>e).operator) {
case SyntaxKind.BarToken: return left | right;
case SyntaxKind.AmpersandToken: return left & right;
case SyntaxKind.PlusToken: return left + right;
case SyntaxKind.MinusToken: return left - right;
case SyntaxKind.GreaterThanGreaterThanToken: return left >> right;
Copy link
Member

Choose a reason for hiding this comment

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

Should support >>> as well probably

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

Choose a reason for hiding this comment

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

Why not include *, /, %, and ^ as well? Seems hard to define a principle that would exclude them.

Copy link
Member

Choose a reason for hiding this comment

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

👍 to Anders' suggestion. Might be arguable as to what you want to do when the RHS for / and % are 0 though.

case SyntaxKind.LessThanLessThanToken: return left << right;
}
return undefined;
case SyntaxKind.NumericLiteral:
return +(<LiteralExpression>e).text;
case SyntaxKind.Identifier:
case SyntaxKind.PropertyAccess:
Copy link
Contributor

Choose a reason for hiding this comment

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

we probably want to handle IndexedAccess as well. That way we can deal with enums with string/numeric names.

if (!program.getCompilerOptions().propagateEnumConstants) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

curlies

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


var enumSymbol: Symbol;
var propertyName: string;

if (e.kind === SyntaxKind.Identifier) {
// unqualified names can refer to member that reside in different declaration of the enum so just doing name resolution won't work.
// instead pick symbol that correspond of enum declaration and later try to fetch member from the symbol
enumSymbol = getSymbolOfNode(member.parent);
propertyName = (<Identifier>e).text;
}
else {
// left part in PropertyAccess should be resolved to the symbol of enum that declared 'member'
enumSymbol = resolveEntityName(member, (<PropertyAccess>e).left, SymbolFlags.Enum, /*suppressErrors*/ true);

if (enumSymbol !== getSymbolOfNode(member.parent)) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

curlies.

propertyName = (<Identifier>(<PropertyAccess>e).right).text;
Copy link
Contributor

Choose a reason for hiding this comment

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

cast to identifier shoudln't be necessary.

}

var propertySymbol = enumSymbol.exports[propertyName];
if (!propertyName || !(propertySymbol.flags & SymbolFlags.EnumMember)) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

enum property name could be "" right? i think you need to check that propertyName is actually undefined.

var propertyDecl = <EnumMember>propertySymbol.valueDeclaration;
// self references are illegal
if (member === propertyDecl) return undefined;
// enumMemberValue might be undefined if corresponding enum value was not yet computed
// and it is ok to return undefined in this case (use before defition)
return <number>getNodeLinks(propertyDecl).enumMemberValue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this work ok in the LS (where we might helicopter in to an arbitrary enum member)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe that this value was never used by language service. In fact we do not compute them when checker is not in fulltypecheck mode.

}
}
}
}

function checkEnumDeclaration(node: EnumDeclaration) {
Expand Down
9 changes: 7 additions & 2 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module ts {
type: "boolean",
},
{
name: "emitBOM",
name: "emitBOM",
type: "boolean"
},
{
Expand Down Expand Up @@ -102,7 +102,7 @@ module ts {
{
name: "target",
shortName: "t",
type: { "es3": ScriptTarget.ES3, "es5": ScriptTarget.ES5 , "es6": ScriptTarget.ES6 },
type: { "es3": ScriptTarget.ES3, "es5": ScriptTarget.ES5, "es6": ScriptTarget.ES6 },
description: Diagnostics.Specify_ECMAScript_target_version_Colon_ES3_default_ES5_or_ES6_experimental,
paramType: Diagnostics.VERSION,
error: Diagnostics.Argument_for_target_option_must_be_es3_es5_or_es6
Expand All @@ -118,6 +118,11 @@ module ts {
shortName: "w",
type: "boolean",
description: Diagnostics.Watch_input_files,
},
{
name: "propagateEnumConstants",
Copy link
Contributor

Choose a reason for hiding this comment

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

i think this should be marked as 'experimental'. Or something to indicate that it is a spec augmentation.

Copy link
Contributor

Choose a reason for hiding this comment

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

i would not put description on it so that it does not show in the main list of options.

type: "boolean",
description: Diagnostics.Propagate_constant_values_in_enum_member_initializers
}
];

Expand Down
1 change: 1 addition & 0 deletions src/compiler/diagnosticInformationMap.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ module ts {
Specify_module_code_generation_Colon_commonjs_or_amd: { code: 6016, category: DiagnosticCategory.Message, key: "Specify module code generation: 'commonjs' or 'amd'" },
Print_this_message: { code: 6017, category: DiagnosticCategory.Message, key: "Print this message." },
Print_the_compiler_s_version: { code: 6019, category: DiagnosticCategory.Message, key: "Print the compiler's version." },
Propagate_constant_values_in_enum_member_initializers: { code: 6020, category: DiagnosticCategory.Message, key: "Propagate constant values in enum member initializers." },
Syntax_Colon_0: { code: 6023, category: DiagnosticCategory.Message, key: "Syntax: {0}" },
options: { code: 6024, category: DiagnosticCategory.Message, key: "options" },
file: { code: 6025, category: DiagnosticCategory.Message, key: "file" },
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,10 @@
"category": "Message",
"code": 6019
},
"Propagate constant values in enum member initializers.": {
"category": "Message",
"code": 6020
},
"Syntax: {0}": {
"category": "Message",
"code": 6023
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,7 @@ module ts {
target?: ScriptTarget;
version?: boolean;
watch?: boolean;
propagateEnumConstants?: boolean;
[option: string]: any;
}

Expand Down
7 changes: 4 additions & 3 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,9 @@ module Harness {
case 'usecasesensitivefilenames':
useCaseSensitiveFileNames = setting.value === 'true';
break;
case 'propagateenumconstants':
options.propagateEnumConstants = setting.value === 'true';
break;

case 'mapsourcefiles':
case 'maproot':
Expand All @@ -757,7 +760,6 @@ module Harness {
case 'codepage':
case 'createFileLog':
case 'filename':
case 'propagateenumconstants':
case 'removecomments':
case 'watch':
case 'allowautomaticsemicoloninsertion':
Expand All @@ -772,7 +774,6 @@ module Harness {
case 'errortruncation':
options.noErrorTruncation = setting.value === 'false';
break;

default:
throw new Error('Unsupported compiler setting ' + setting.flag);
}
Expand Down Expand Up @@ -1147,7 +1148,7 @@ module Harness {
var optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*(\S*)/gm; // multiple matches on multiple lines

// List of allowed metadata names
var fileMetadataNames = ["filename", "comments", "declaration", "module", "nolib", "sourcemap", "target", "out", "outdir", "noimplicitany", "noresolve", "newline", "newlines", "emitbom", "errortruncation", "usecasesensitivefilenames"];
var fileMetadataNames = ["filename", "comments", "declaration", "module", "nolib", "sourcemap", "target", "out", "outdir", "noimplicitany", "noresolve", "newline", "newlines", "emitbom", "errortruncation", "usecasesensitivefilenames", "propagateenumconstants"];

function extractCompilerSettings(content: string): CompilerSetting[] {

Expand Down
153 changes: 153 additions & 0 deletions tests/baselines/reference/constantsInEnumMembers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//// [constantsInEnumMembers.ts]

enum Enum1 {
A0 = 100,
}

enum Enum1 {
// correct cases
A,
B,
C = 10,
D = A + B,
E = A + 1,
F = 1 + A,
G = 1 + 1,
H = A - B,
I = A - 1,
J = 1 - A,
K = 1 - 1,
L = ~D,
M = E << B,
N = E << 1,
O = E >> B,
P = E >> 1,
Q = -D,
R = C & 5,
S = 5 & C,
T = C | D,
U = C | 1,
V = 10 | D,
W = Enum1.V,

// correct cases: reference to the enum member from different enum declaration
W1 = A0,
W2 = Enum1.A0,
Copy link
Contributor

Choose a reason for hiding this comment

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

can you have a version with a more complicated property access? like x.y.Enum1.A0.


// illegal case
// forward reference to the element of the same enum
X = Y,
// forward reference to the element of the same enum
Y = Enum1.Z,
Z = 100,
}


function foo(x: Enum1) {
switch (x) {
case Enum1.A:
case Enum1.B:
case Enum1.C:
case Enum1.D:
case Enum1.E:
case Enum1.F:
case Enum1.G:
case Enum1.H:
case Enum1.I:
case Enum1.J:
case Enum1.K:
case Enum1.L:
case Enum1.M:
case Enum1.N:
case Enum1.O:
case Enum1.P:
case Enum1.Q:
case Enum1.R:
case Enum1.S:
case Enum1.T:
case Enum1.U:
case Enum1.V:
case Enum1.W:
case Enum1.W1:
case Enum1.W2:
case Enum1.X:
case Enum1.Y:
case Enum1.Z:
break;
}
}

//// [constantsInEnumMembers.js]
var Enum1;
(function (Enum1) {
Enum1[Enum1["A0"] = 100] = "A0";
})(Enum1 || (Enum1 = {}));
var Enum1;
(function (Enum1) {
// correct cases
Enum1[Enum1["A"] = 0] = "A";
Enum1[Enum1["B"] = 1] = "B";
Enum1[Enum1["C"] = 10] = "C";
Enum1[Enum1["D"] = A + B] = "D";
Enum1[Enum1["E"] = A + 1] = "E";
Enum1[Enum1["F"] = 1 + A] = "F";
Enum1[Enum1["G"] = 1 + 1] = "G";
Enum1[Enum1["H"] = A - B] = "H";
Enum1[Enum1["I"] = A - 1] = "I";
Enum1[Enum1["J"] = 1 - A] = "J";
Enum1[Enum1["K"] = 1 - 1] = "K";
Enum1[Enum1["L"] = ~D] = "L";
Enum1[Enum1["M"] = E << B] = "M";
Enum1[Enum1["N"] = E << 1] = "N";
Enum1[Enum1["O"] = E >> B] = "O";
Enum1[Enum1["P"] = E >> 1] = "P";
Enum1[Enum1["Q"] = -D] = "Q";
Enum1[Enum1["R"] = C & 5] = "R";
Enum1[Enum1["S"] = 5 & C] = "S";
Enum1[Enum1["T"] = C | D] = "T";
Enum1[Enum1["U"] = C | 1] = "U";
Enum1[Enum1["V"] = 10 | D] = "V";
Enum1[Enum1["W"] = Enum1.V] = "W";
// correct cases: reference to the enum member from different enum declaration
Enum1[Enum1["W1"] = A0] = "W1";
Enum1[Enum1["W2"] = Enum1.A0] = "W2";
// illegal case
// forward reference to the element of the same enum
Enum1[Enum1["X"] = Enum1.Y] = "X";
// forward reference to the element of the same enum
Enum1[Enum1["Y"] = 100 /* Z */] = "Y";
Enum1[Enum1["Z"] = 100] = "Z";
})(Enum1 || (Enum1 = {}));
function foo(x) {
switch (x) {
case 0 /* A */:
case 1 /* B */:
case 10 /* C */:
case 1 /* D */:
case 1 /* E */:
case 1 /* F */:
case 2 /* G */:
case -1 /* H */:
case -1 /* I */:
case 1 /* J */:
case 0 /* K */:
case -2 /* L */:
case 2 /* M */:
case 2 /* N */:
case 0 /* O */:
case 0 /* P */:
case -1 /* Q */:
case 0 /* R */:
case 0 /* S */:
case 11 /* T */:
case 11 /* U */:
case 11 /* V */:
case 11 /* W */:
case 100 /* W1 */:
case 100 /* W2 */:
case Enum1.X:
case Enum1.Y:
case 100 /* Z */:
break;
}
}
Loading