Report deprecation for object literal assignments

This commit is contained in:
Yuya Tanaka 2021-10-02 06:52:02 +09:00
parent a02a7ab8e9
commit 5a06bd3c73
6 changed files with 258 additions and 4 deletions

View file

@ -16924,7 +16924,7 @@ namespace ts {
containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined,
errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined
): boolean {
if (isTypeRelatedTo(source, target, relation)) return true;
if (isTypeRelatedTo(source, target, relation, /*reportDeprecatedProperties*/ true)) return true;
if (!errorNode || !elaborateError(expr, source, target, relation, headMessage, containingMessageChain, errorOutputContainer)) {
return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer);
}
@ -17693,7 +17693,7 @@ namespace ts {
return false;
}
function isTypeRelatedTo(source: Type, target: Type, relation: ESMap<string, RelationComparisonResult>) {
function isTypeRelatedTo(source: Type, target: Type, relation: ESMap<string, RelationComparisonResult>, reportDeprecatedProperties?: boolean) {
if (isFreshLiteralType(source)) {
source = (source as FreshableType).regularType;
}
@ -17719,7 +17719,9 @@ namespace ts {
}
}
if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) {
return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined);
return checkTypeRelatedTo(source, target, relation,
/*errorNode*/ undefined, /*headMessage*/ undefined, /*containingMessageChain*/ undefined, /*errorOutputContainer*/ undefined,
reportDeprecatedProperties);
}
return false;
}
@ -17753,6 +17755,7 @@ namespace ts {
* @param headMessage If the error chain should be prepended by a head message, then headMessage will be used.
* @param containingMessageChain A chain of errors to prepend any new errors found.
* @param errorOutputContainer Return the diagnostic. Do not log if 'skipLogging' is truthy.
* @param reportDeprecatedProperties Properties with deprecated annotation will be reported, if result is true.
*/
function checkTypeRelatedTo(
source: Type,
@ -17762,6 +17765,7 @@ namespace ts {
headMessage?: DiagnosticMessage,
containingMessageChain?: () => DiagnosticMessageChain | undefined,
errorOutputContainer?: { errors?: Diagnostic[], skipLogging?: boolean },
reportDeprecatedProperties?: boolean
): boolean {
let errorInfo: DiagnosticMessageChain | undefined;
@ -17778,6 +17782,7 @@ namespace ts {
let lastSkippedInfo: [Type, Type] | undefined;
let incompatibleStack: [DiagnosticMessage, (string | number)?, (string | number)?, (string | number)?, (string | number)?][] = [];
let inPropertyCheck = false;
const deprecatedSuggestions: Parameters<typeof addDeprecatedSuggestion>[] = [];
Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking");
@ -17829,8 +17834,15 @@ namespace ts {
Debug.assert(!!errorOutputContainer.errors, "missed opportunity to interact with error.");
}
const isRelated = result !== Ternary.False;
return result !== Ternary.False;
if (isRelated) {
for (const args of deprecatedSuggestions) {
addDeprecatedSuggestion(...args);
}
}
return isRelated;
function resetErrorInfo(saved: ReturnType<typeof captureErrorCalculationState>) {
errorInfo = saved.errorInfo;
@ -18164,6 +18176,9 @@ namespace ts {
}
return Ternary.False;
}
if (reportDeprecatedProperties) {
checkDeprecatedProperties(source as FreshObjectLiteralType, target);
}
}
const isPerformingCommonPropertyChecks = relation !== comparableRelation && !(intersectionState & IntersectionState.Target) &&
@ -18441,6 +18456,27 @@ namespace ts {
return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration;
}
function checkDeprecatedProperties(source: FreshObjectLiteralType, target: Type) {
if (!isExcessPropertyCheckTarget(target)) {
return;
}
for (const prop of getPropertiesOfType(source)) {
if (!shouldCheckAsExcessProperty(prop, source.symbol)) {
continue;
}
const symbol = getPropertyOfObjectType(target, prop.escapedName);
if (symbol?.declarations?.some(decl => decl.flags & NodeFlags.Deprecated)) {
const node = prop.valueDeclaration!;
const nameNode = isPropertyAssignment(node) || isShorthandPropertyAssignment(node) || isObjectLiteralMethod(node) || isJsxAttribute(node)
? node.name
: prop.valueDeclaration!;
deprecatedSuggestions.push([nameNode, symbol.declarations, prop.escapedName as string]);
}
}
}
function eachTypeRelatedToSomeType(source: UnionOrIntersectionType, target: UnionOrIntersectionType): Ternary {
let result = Ternary.True;
const sourceTypes = source.types;

View file

@ -0,0 +1,57 @@
//// interface Props {
//// /** @deprecated */
//// foo?: string;
//// bar: string;
//// baz?: {
//// /** @deprecated */
//// foo?: boolean;
//// bar?: boolean;
//// };
//// /** @deprecated */
//// callback?: () => void
//// }
//// const Component = (props: Props) => props && <div />;
//// const foo = "foo";
//// const bar = "bar";
//// const callback = () => {};
//// let props: Props = { [|callback|]: () => {}, [|foo|]: "foo", bar: "bar", baz: { [|foo|]: true } };
//// props = { [|"foo"|]: "foo", "bar": "bar" };
//// props = { [|["foo"]|]: "foo", ["bar"]: "bar" };
//// props = { [|foo|], bar, [|callback|] };
//// props = { bar, [|callback|]() {} };
//// // Skip if there is a type incompatibility error.
//// const props5: Props = { foo: "foo", boo: "boo" };
//// // Skip for union types.
//// const props6: { foo: { /** @deprecated */ bar: string } | { bar: string, baz: string } } = { foo: { bar: "bar" } };
const ranges = test.ranges();
verify.getSuggestionDiagnostics([
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[0],
reportsDeprecated: true,
},
...ranges.slice(1, ranges.length - 2).map(range => ({
message: "'foo' is deprecated.",
code: 6385,
range,
reportsDeprecated: true as const,
})),
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[ranges.length - 2],
reportsDeprecated: true,
},
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[ranges.length - 1],
reportsDeprecated: true,
},
])

View file

@ -0,0 +1,58 @@
//// interface Props {
//// /** @deprecated */
//// foo?: string;
//// bar: string;
//// baz?: {
//// /** @deprecated */
//// foo?: boolean;
//// bar?: boolean;
//// };
//// /** @deprecated */
//// callback?: () => void
//// }
//// const func = (_props: Props) => {};
//// const foo = "foo";
//// const bar = "bar";
//// const callback = () => {};
//// func({ [|callback|]: () => {}, [|foo|]: "foo", bar: "bar", baz: { [|foo|]: true } });
//// func({ [|"foo"|]: "foo", "bar": "bar" });
//// func({ [|["foo"]|]: "foo", ["bar"]: "bar" });
//// func({ [|foo|], bar, [|callback|] });
//// func({ bar, [|callback|]() {} });
//// // Skip if there is a type incompatibility error.
//// func({ foo: "foo", boo: "boo" });
//// // Skip for union types.
//// function test(_args: { foo: { /** @deprecated */ bar: string } | { bar: string, baz: string } }) {}
//// test({ foo: { bar: "bar" } })
const ranges = test.ranges();
verify.getSuggestionDiagnostics([
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[0],
reportsDeprecated: true,
},
...ranges.slice(1, ranges.length - 2).map(range => ({
message: "'foo' is deprecated.",
code: 6385,
range,
reportsDeprecated: true as const,
})),
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[ranges.length - 2],
reportsDeprecated: true,
},
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[ranges.length - 1],
reportsDeprecated: true,
},
])

View file

@ -0,0 +1,55 @@
// @Filename: a.tsx
//// type Props = {
//// /** @deprecated */
//// foo?: string;
//// bar: string;
//// baz?: {
//// /** @deprecated */
//// foo?: boolean;
//// bar?: boolean;
//// };
//// /** @deprecated */
//// callback?: () => void
//// /** @deprecated */
//// bool?: boolean;
//// }
//// const Component = (props: Props) => props && <div />;
//// const foo = "foo";
//// const bar = "bar";
//// <Component [|bool|] [|callback|]={() => {}} [|foo|]="foo" bar="bar" baz={{ [|foo|]: true }} />;
//// // Skip spread in jsx.
//// <Component {...{ foo: "foo", bar: "bar" }} />;
//// // Skip if there is a type incompatibility error.
//// <Component foo="" boo="" />;
//// <Component {...{ foo: "foo", boo: "boo" }} />;
//// // Skip for union types.
//// const Component2 = (_props: { foo: { /** @deprecated */ bar: string } | { bar: string, baz: string } }) => <div />;
//// <Component foo={{ bar: "bar" }} />;
goTo.file('a.tsx')
const ranges = test.ranges();
verify.getSuggestionDiagnostics([
{
message: "'bool' is deprecated.",
code: 6385,
range: ranges[0],
reportsDeprecated: true,
},
{
message: "'callback' is deprecated.",
code: 6385,
range: ranges[1],
reportsDeprecated: true,
},
...ranges.slice(2).map(range => ({
message: "'foo' is deprecated.",
code: 6385,
range,
reportsDeprecated: true as const,
}))
])

View file

@ -0,0 +1,25 @@
// @Filename: a.tsx
//// function OverridableComponent(props: {
//// /** @deprecated */
//// a: boolean;
//// c?: boolean;
//// }): JSX.Element
//// function OverridableComponent(props: { a: boolean; b: boolean }): JSX.Element
//// function OverridableComponent(_props: { a: boolean; b?: boolean; c?: boolean }) {
//// return <div />;
//// }
//// <OverridableComponent [|a|] />;
//// <OverridableComponent a b />;
goTo.file('a.tsx')
const ranges = test.ranges();
verify.getSuggestionDiagnostics([
{
message: "'a' is deprecated.",
code: 6385,
range: ranges[0],
reportsDeprecated: true,
}
])

View file

@ -0,0 +1,23 @@
//// function overloadFunc(props: {
//// /** @deprecated */
//// a: boolean;
//// c?: boolean;
//// }): JSX.Element
//// function overloadFunc(props: { a: boolean; b: boolean }): JSX.Element
//// function overloadFunc(_props: { a: boolean; b?: boolean; c?: boolean }) {
//// return <div />;
//// }
//// overloadFunc({ [|a|]: true }) />;
//// overloadFunc({ a: true, b: true }) />;
const ranges = test.ranges();
verify.getSuggestionDiagnostics([
{
message: "'a' is deprecated.",
code: 6385,
range: ranges[0],
reportsDeprecated: true,
}
])