Skip to content
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

Weak type detection #16047

Merged
merged 20 commits into from
Jun 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 68 additions & 34 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2285,7 +2285,7 @@ namespace ts {
Debug.assert(typeNode !== undefined, "should always get typenode?");
const options = { removeComments: true };
const writer = createTextWriter("");
const printer = createPrinter(options, writer);
const printer = createPrinter(options);
const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration);
printer.writeNode(EmitHint.Unspecified, typeNode, /*sourceFile*/ sourceFile, writer);
const result = writer.getText();
Expand Down Expand Up @@ -8701,6 +8701,7 @@ namespace ts {
let expandingFlags: number;
let depth = 0;
let overflow = false;
let isIntersectionConstituent = false;

Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking");

Expand Down Expand Up @@ -8783,7 +8784,6 @@ namespace ts {
* * Ternary.False if they are not related.
*/
function isRelatedTo(source: Type, target: Type, reportErrors?: boolean, headMessage?: DiagnosticMessage): Ternary {
let result: Ternary;
if (source.flags & TypeFlags.StringOrNumberLiteral && source.flags & TypeFlags.FreshLiteral) {
source = (<LiteralType>source).regularType;
}
Expand Down Expand Up @@ -8815,32 +8815,39 @@ namespace ts {
}
}

if (!(source.flags & TypeFlags.UnionOrIntersection) &&
!(target.flags & TypeFlags.Union) &&
!isIntersectionConstituent &&
source !== globalObjectType &&
getPropertiesOfType(source).length > 0 &&
isWeakType(target) &&
!hasCommonProperties(source, target)) {
if (reportErrors) {
reportError(Diagnostics.Type_0_has_no_properties_in_common_with_type_1, typeToString(source), typeToString(target));
}
return Ternary.False;
}

let result = Ternary.False;
const saveErrorInfo = errorInfo;
const saveIsIntersectionConstituent = isIntersectionConstituent;
isIntersectionConstituent = false;

// Note that these checks are specifically ordered to produce correct results. In particular,
// we need to deconstruct unions before intersections (because unions are always at the top),
// and we need to handle "each" relations before "some" relations for the same kind of type.
if (source.flags & TypeFlags.Union) {
if (relation === comparableRelation) {
result = someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive));
}
else {
result = eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive));
}
if (result) {
return result;
}
result = relation === comparableRelation ?
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)) :
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive));
}
else {
if (target.flags & TypeFlags.Union) {
if (result = typeRelatedToSomeType(source, <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive))) {
return result;
}
result = typeRelatedToSomeType(source, <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
}
else if (target.flags & TypeFlags.Intersection) {
if (result = typeRelatedToEachType(source, target as IntersectionType, reportErrors)) {
return result;
}
isIntersectionConstituent = true;
result = typeRelatedToEachType(source, target as IntersectionType, reportErrors);
}
else if (source.flags & TypeFlags.Intersection) {
// Check to see if any constituents of the intersection are immediately related to the target.
Expand All @@ -8856,20 +8863,18 @@ namespace ts {
//
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
// breaking the intersection apart.
if (result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false)) {
return result;
}
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false);
}

if (source.flags & TypeFlags.StructuredOrTypeVariable || target.flags & TypeFlags.StructuredOrTypeVariable) {
if (!result && (source.flags & TypeFlags.StructuredOrTypeVariable || target.flags & TypeFlags.StructuredOrTypeVariable)) {
if (result = recursiveTypeRelatedTo(source, target, reportErrors)) {
errorInfo = saveErrorInfo;
return result;
}
}
}

if (reportErrors) {
isIntersectionConstituent = saveIsIntersectionConstituent;

if (!result && reportErrors) {
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Primitive) {
tryElaborateErrorsForPrimitivesAndObjects(source, target);
}
Expand All @@ -8878,7 +8883,7 @@ namespace ts {
}
reportRelationError(headMessage, source, target);
}
return Ternary.False;
return result;
}

function isIdenticalTo(source: Type, target: Type): Ternary {
Expand Down Expand Up @@ -8977,7 +8982,7 @@ namespace ts {
}
}

function typeRelatedToEachType(source: Type, target: UnionOrIntersectionType, reportErrors: boolean): Ternary {
function typeRelatedToEachType(source: Type, target: IntersectionType, reportErrors: boolean): Ternary {
let result = Ternary.True;
const targetTypes = target.types;
for (const targetType of targetTypes) {
Expand Down Expand Up @@ -9279,7 +9284,6 @@ namespace ts {
const requireOptionalProperties = relation === subtypeRelation && !(getObjectFlags(source) & ObjectFlags.ObjectLiteral);
for (const targetProp of properties) {
const sourceProp = getPropertyOfType(source, targetProp.name);

if (sourceProp !== targetProp) {
if (!sourceProp) {
if (!(targetProp.flags & SymbolFlags.Optional) || requireOptionalProperties) {
Expand Down Expand Up @@ -9358,6 +9362,34 @@ namespace ts {
return result;
}

/**
* A type is 'weak' if it is an object type with at least one optional property
* and no required properties, call/construct signatures or index signatures
*/
function isWeakType(type: Type): boolean {
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 &&
!resolved.stringIndexInfo && !resolved.numberIndexInfo &&
resolved.properties.length > 0 &&
every(resolved.properties, p => !!(p.flags & SymbolFlags.Optional));
}
if (type.flags & TypeFlags.Intersection) {
return every((<IntersectionType>type).types, isWeakType);
}
return false;
}

function hasCommonProperties(source: Type, target: Type) {
const isComparingJsxAttributes = !!(source.flags & TypeFlags.JsxAttributes);
for (const prop of getPropertiesOfType(source)) {
if (isKnownProperty(target, prop.name, isComparingJsxAttributes)) {
return true;
}
}
return false;
}

function propertiesIdenticalTo(source: Type, target: Type): Ternary {
if (!(source.flags & TypeFlags.Object && target.flags & TypeFlags.Object)) {
return Ternary.False;
Expand Down Expand Up @@ -14121,8 +14153,10 @@ namespace ts {
function isKnownProperty(targetType: Type, name: string, isComparingJsxAttributes: boolean): boolean {
if (targetType.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(<ObjectType>targetType);
if (resolved.stringIndexInfo || resolved.numberIndexInfo && isNumericLiteralName(name) ||
getPropertyOfType(targetType, name) || isComparingJsxAttributes && !isUnhyphenatedJsxName(name)) {
if (resolved.stringIndexInfo ||
resolved.numberIndexInfo && isNumericLiteralName(name) ||
getPropertyOfType(targetType, name) ||
isComparingJsxAttributes && !isUnhyphenatedJsxName(name)) {
// For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known.
return true;
}
Expand Down Expand Up @@ -22671,12 +22705,12 @@ namespace ts {
return symbols;
}
else if (symbol.flags & SymbolFlags.Transient) {
if ((symbol as SymbolLinks).leftSpread) {
const links = symbol as SymbolLinks;
return [...getRootSymbols(links.leftSpread), ...getRootSymbols(links.rightSpread)];
const transient = symbol as TransientSymbol;
if (transient.leftSpread) {
return [...getRootSymbols(transient.leftSpread), ...getRootSymbols(transient.rightSpread)];
}
if ((symbol as SymbolLinks).syntheticOrigin) {
return getRootSymbols((symbol as SymbolLinks).syntheticOrigin);
if (transient.syntheticOrigin) {
return getRootSymbols(transient.syntheticOrigin);
}

let target: Symbol;
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,10 @@
"category": "Error",
"code": 2558
},
"Type '{0}' has no properties in common with type '{1}'.": {
"category": "Error",
"code": 2559
},
"JSX element attributes type '{0}' may not be a union type.": {
"category": "Error",
"code": 2600
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/APISample_watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare var fs: {
existsSync(path: string): boolean;
readdirSync(path: string): string[];
readFileSync(filename: string, encoding?: string): string;
writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void;
writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; } | string): void;
watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: (curr: { mtime: Date }, prev: { mtime: Date }) => void): void;
};
declare var path: any;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(33,5): error TS2559: Type 'D' has no properties in common with type 'C'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(34,5): error TS2559: Type 'E' has no properties in common with type 'C'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(35,5): error TS2559: Type 'F' has no properties in common with type 'C'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(36,5): error TS2559: Type 'D' has no properties in common with type '{ opt?: Base; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(37,5): error TS2559: Type 'E' has no properties in common with type '{ opt?: Base; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(38,5): error TS2559: Type 'F' has no properties in common with type '{ opt?: Base; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(39,5): error TS2559: Type 'D' has no properties in common with type '{ opt?: Base; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(40,5): error TS2559: Type 'E' has no properties in common with type '{ opt?: Base; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(41,5): error TS2559: Type 'F' has no properties in common with type '{ opt?: Base; }'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(74,5): error TS2322: Type 'D' is not assignable to type 'C'.
Property 'opt' is missing in type 'D'.
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts(75,5): error TS2322: Type 'E' is not assignable to type 'C'.
Expand All @@ -18,7 +27,7 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignme
Property 'opt' is missing in type 'F'.


==== tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts (9 errors) ====
==== tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithObjectMembersOptionality2.ts (18 errors) ====
// M is optional and S contains no property with the same name as M
// N is optional and T contains no property with the same name as N

Expand Down Expand Up @@ -50,20 +59,38 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignme
var e: E;
var f: F;

// all ok
// disallowed by weak type checking
c = d;
~
!!! error TS2559: Type 'D' has no properties in common with type 'C'.
c = e;
~
!!! error TS2559: Type 'E' has no properties in common with type 'C'.
c = f;
c = a;
~
!!! error TS2559: Type 'F' has no properties in common with type 'C'.
a = d;
~
!!! error TS2559: Type 'D' has no properties in common with type '{ opt?: Base; }'.
a = e;
~
!!! error TS2559: Type 'E' has no properties in common with type '{ opt?: Base; }'.
a = f;
a = c;
~
!!! error TS2559: Type 'F' has no properties in common with type '{ opt?: Base; }'.
b = d;
~
!!! error TS2559: Type 'D' has no properties in common with type '{ opt?: Base; }'.
b = e;
~
!!! error TS2559: Type 'E' has no properties in common with type '{ opt?: Base; }'.
b = f;
~
!!! error TS2559: Type 'F' has no properties in common with type '{ opt?: Base; }'.

// ok
c = a;
a = c;
b = a;
b = c;
}
Expand Down Expand Up @@ -134,4 +161,5 @@ tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignme
!!! error TS2322: Property 'opt' is missing in type 'F'.
b = a; // ok
b = c; // ok
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ module TargetHasOptional {
var e: E;
var f: F;

// all ok
// disallowed by weak type checking
c = d;
c = e;
c = f;
c = a;

a = d;
a = e;
a = f;
a = c;

b = d;
b = e;
b = f;

// ok
c = a;
a = c;
b = a;
b = c;
}
Expand Down Expand Up @@ -87,7 +87,8 @@ module SourceHasOptional {
b = f; // error
b = a; // ok
b = c; // ok
}
}


//// [assignmentCompatWithObjectMembersOptionality2.js]
// M is optional and S contains no property with the same name as M
Expand Down Expand Up @@ -129,18 +130,19 @@ var TargetHasOptional;
var d;
var e;
var f;
// all ok
// disallowed by weak type checking
c = d;
c = e;
c = f;
c = a;
a = d;
a = e;
a = f;
a = c;
b = d;
b = e;
b = f;
// ok
c = a;
a = c;
b = a;
b = c;
})(TargetHasOptional || (TargetHasOptional = {}));
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/checkJsxChildrenProperty1.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ interface Prop {

children: string | JSX.Element
>children : Symbol(Prop.children, Decl(file.tsx, 4, 14))
>JSX : Symbol(JSX, Decl(react.d.ts, 2352, 1))
>Element : Symbol(JSX.Element, Decl(react.d.ts, 2355, 27))
>JSX : Symbol(JSX, Decl(react.d.ts, 2353, 1))
>Element : Symbol(JSX.Element, Decl(react.d.ts, 2356, 27))
}

function Comp(p: Prop) {
Expand All @@ -23,11 +23,11 @@ function Comp(p: Prop) {
>Prop : Symbol(Prop, Decl(file.tsx, 0, 32))

return <div>{p.b}</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2399, 45))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2400, 45))
>p.b : Symbol(Prop.b, Decl(file.tsx, 3, 14))
>p : Symbol(p, Decl(file.tsx, 8, 14))
>b : Symbol(Prop.b, Decl(file.tsx, 3, 14))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2399, 45))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2400, 45))
}

// OK
Expand Down Expand Up @@ -59,8 +59,8 @@ let k2 =
>b : Symbol(b, Decl(file.tsx, 19, 16))

<div>hi hi hi!</div>
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2399, 45))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2399, 45))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2400, 45))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react.d.ts, 2400, 45))

</Comp>;
>Comp : Symbol(Comp, Decl(file.tsx, 6, 1))
Expand Down
Loading