From a354f079198ba7c95ee488963ba6f54d1ac922c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EB=B3=91=EC=A7=84?= <64676070+sunrabbit123@users.noreply.github.com> Date: Fri, 1 Sep 2023 18:09:32 +0900 Subject: [PATCH] feat: Implement property access about non-primitive constraints of IAT (#1067) **Description:** ```ts function l(s: string, tp: T[P]): void { tp = s; } function m(s: string, tp: T[P]): void { tp = s; } function f(s: string, tp: T[P]): void { tp = s; } ``` --- crates/stc_ts_errors/src/lib.rs | 10 ++ .../src/analyzer/assign/mod.rs | 35 +++++++ .../src/analyzer/expr/mod.rs | 92 +++++++++++++++++-- .../src/analyzer/types/keyof.rs | 7 +- .../src/analyzer/types/mod.rs | 7 ++ .../tests/pass-only/types/keyof/3.ts | 19 ++++ .../tests/pass-only/types/mapped/.1.ts | 6 ++ .../tests/tsc/types/mapped/1.ts | 23 +++++ .../tests/tsc/types/mapped/1.tsc-errors.json | 14 +++ .../tests/tsc/types/mapped/2.ts | 7 ++ .../tests/tsc/types/mapped/2.tsc-errors.json | 14 +++ .../tsc/types/nonPrimitive/{.1.ts => 1.ts} | 5 +- .../tsc/types/nonPrimitive/1.tsc-errors.json | 20 ++++ .../tests/conformance.pass.txt | 1 + .../mappedTypeRelationships.error-diff.json | 28 ++---- .../mappedTypeRelationships.stats.rust-debug | 6 +- ...onstraintOfIndexAccessType.error-diff.json | 12 --- ...nstraintOfIndexAccessType.stats.rust-debug | 4 +- .../tests/tsc-stats.rust-debug | 6 +- 19 files changed, 268 insertions(+), 48 deletions(-) create mode 100644 crates/stc_ts_file_analyzer/tests/pass-only/types/keyof/3.ts create mode 100644 crates/stc_ts_file_analyzer/tests/pass-only/types/mapped/.1.ts create mode 100644 crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.ts create mode 100644 crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.tsc-errors.json create mode 100644 crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.ts create mode 100644 crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.tsc-errors.json rename crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/{.1.ts => 1.ts} (68%) create mode 100644 crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.tsc-errors.json delete mode 100644 crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.error-diff.json diff --git a/crates/stc_ts_errors/src/lib.rs b/crates/stc_ts_errors/src/lib.rs index 7bdf5ff2c4..9d8087a4d2 100644 --- a/crates/stc_ts_errors/src/lib.rs +++ b/crates/stc_ts_errors/src/lib.rs @@ -1589,6 +1589,11 @@ pub enum ErrorKind { RestParamMustBeLast { span: Span, }, + + // TS2536 + CannotBeUsedToIndexType { + span: Span, + }, } #[cfg(target_pointer_width = "64")] @@ -2203,6 +2208,7 @@ impl ErrorKind { ErrorKind::ImportFailed { .. } => 2305, + ErrorKind::CannotBeUsedToIndexType { .. } => 2536, _ => 0, } } @@ -2236,6 +2242,10 @@ impl ErrorKind { matches!(self, Self::NoSuchType { .. } | Self::NoSuchTypeButVarExists { .. }) } + pub fn is_cannot_be_used_index_ty(&self) -> bool { + self.code() == 2536 + } + #[cold] pub fn flatten(vec: Vec) -> Vec { let mut buf = Vec::with_capacity(vec.len()); diff --git a/crates/stc_ts_file_analyzer/src/analyzer/assign/mod.rs b/crates/stc_ts_file_analyzer/src/analyzer/assign/mod.rs index 88e05ecec3..965cdac254 100644 --- a/crates/stc_ts_file_analyzer/src/analyzer/assign/mod.rs +++ b/crates/stc_ts_file_analyzer/src/analyzer/assign/mod.rs @@ -1179,6 +1179,41 @@ impl Analyzer<'_, '_> { return Ok(()); } + // function f3(x: T, y: U, k: keyof T) { + // x[k] = y[k]; + // } + ( + Type::IndexedAccessType(IndexedAccessType { + obj_type: box Type::Param(TypeParam { name: lhs_name, .. }), + index_type: lhs_idx, + .. + }), + Type::IndexedAccessType(IndexedAccessType { + obj_type: + box Type::Param(TypeParam { + constraint: Some(box Type::Param(TypeParam { name: rhs_name, .. })), + .. + }), + index_type: rhs_idx, + .. + }), + ) if lhs_name.eq(rhs_name) && lhs_idx.is_index() && rhs_idx.is_index() => { + if matches!(( + lhs_idx.as_index().map(|ty| ty.ty.normalize()), + rhs_idx.as_index().map(|ty| ty.ty.normalize()), + ), ( + Some(Type::Param(TypeParam { + name: lhs_ty_param_name, .. + })), + Some(Type::Param(TypeParam { + name: rhs_ty_param_name, .. + })), + ) if lhs_ty_param_name.eq(rhs_ty_param_name)) + { + return Ok(()); + } + } + _ => {} } diff --git a/crates/stc_ts_file_analyzer/src/analyzer/expr/mod.rs b/crates/stc_ts_file_analyzer/src/analyzer/expr/mod.rs index 9c883879fa..ef72faaba3 100644 --- a/crates/stc_ts_file_analyzer/src/analyzer/expr/mod.rs +++ b/crates/stc_ts_file_analyzer/src/analyzer/expr/mod.rs @@ -2174,14 +2174,93 @@ impl Analyzer<'_, '_> { .. }) => { { - // Check for `T extends { a: { x: any } }` - if let Some(constraint) = constraint { - let ctx = Ctx { - ignore_errors: true, - ..self.ctx + let validate_type_param = |name: &Id| { + match prop.ty().normalize() { + Type::Param(TypeParam { constraint: Some(ty), .. }) => { + if let Type::Index(Index { + ty: box Type::Param(TypeParam { name: constraint_name, .. }), + .. + }) = ty.normalize() + { + if !name.eq(constraint_name) { + return Err(ErrorKind::CannotBeUsedToIndexType { span }.into()); + } + } + } + + Type::Index(Index { + ty: box Type::Param(TypeParam { name: constraint_name, .. }), + .. + }) if !name.eq(constraint_name) => return Err(ErrorKind::CannotBeUsedToIndexType { span }.into()), + _ => {} + } + Ok(()) + }; + + if let Some(ty) = constraint { + if let Type::Param(TypeParam { name: constraint_name, .. }) = ty.normalize() { + match validate_type_param(name) { + Ok(..) => {} + Err(err) => validate_type_param(constraint_name)?, + }; + } + } else { + match validate_type_param(name) { + Ok(..) => {} + Err(err) => return Err(err), }; + } + } + + // Check for `T extends { a: { x: any } }` + if let Some(constraint) = constraint { + let ctx = Ctx { + ignore_errors: true, + ..self.ctx + }; + + let accessible = matches!( + constraint.normalize(), + Type::Keyword(KeywordType { + kind: TsKeywordTypeKind::TsObjectKeyword, + .. + }) + ) || if let Type::Param(TypeParam { + constraint: Some(ty), + name: origin_name, + .. + }) = prop.ty().normalize() + { + if name.eq(origin_name) { + true + } else { + if let Type::Index(Index { + ty: box Type::Param(TypeParam { name: constraint_name, .. }), + .. + }) = ty.normalize() + { + !name.eq(constraint_name) + } else { + true + } + } + } else { + true + }; + + if accessible { if let Ok(ty) = self.with_ctx(ctx).access_property(span, constraint, prop, type_mode, id_ctx, opts) { - return Ok(ty); + if let Type::IndexedAccessType(IndexedAccessType { + obj_type: box Type::Param(TypeParam { name: constraint_name, .. }), + .. + }) = ty.normalize() + { + if name.eq(constraint_name) { + return Ok(ty); + } + } else { + return Ok(ty); + } } } } @@ -2220,7 +2299,6 @@ impl Analyzer<'_, '_> { } warn!("Creating an indexed access type with type parameter as the object"); - return Ok(Type::IndexedAccessType(IndexedAccessType { span, readonly: false, diff --git a/crates/stc_ts_file_analyzer/src/analyzer/types/keyof.rs b/crates/stc_ts_file_analyzer/src/analyzer/types/keyof.rs index e74754749f..d43a378a89 100644 --- a/crates/stc_ts_file_analyzer/src/analyzer/types/keyof.rs +++ b/crates/stc_ts_file_analyzer/src/analyzer/types/keyof.rs @@ -6,7 +6,7 @@ use stc_ts_errors::{debug::force_dump_type_as_string, DebugExt, ErrorKind}; use stc_ts_type_ops::{is_str_lit_or_union, Fix}; use stc_ts_types::{ Class, ClassMember, ClassProperty, Index, KeywordType, KeywordTypeMetadata, LitType, Method, MethodSignature, PropertySignature, - Readonly, Ref, Type, TypeElement, + Readonly, Ref, Type, TypeElement, TypeParam, }; use stc_utils::{cache::Freeze, ext::TypeVecExt, stack, try_cache}; use swc_atoms::js_word; @@ -313,7 +313,10 @@ impl Analyzer<'_, '_> { return Ok(Type::new_union(span, key_types)); } - Type::Param(..) => { + Type::Param(TypeParam { constraint, .. }) => { + // if let Some(box c) = constraint { + // return self.keyof(span, c); + // } return Ok(Type::Index(Index { span, ty: Box::new(ty.into_owned()), diff --git a/crates/stc_ts_file_analyzer/src/analyzer/types/mod.rs b/crates/stc_ts_file_analyzer/src/analyzer/types/mod.rs index 119bbe5f6e..851ec77592 100644 --- a/crates/stc_ts_file_analyzer/src/analyzer/types/mod.rs +++ b/crates/stc_ts_file_analyzer/src/analyzer/types/mod.rs @@ -581,6 +581,13 @@ impl Analyzer<'_, '_> { ); if result.is_err() { + match result { + Err(err) if err.is_cannot_be_used_index_ty() => { + return Err(err); + } + _ => (), + } + if let Type::Param(TypeParam { constraint: Some(ty), .. }) = index_ty.normalize() { let prop = self.normalize(span, Cow::Borrowed(ty), opts)?.into_owned(); result = self.with_ctx(ctx).access_property( diff --git a/crates/stc_ts_file_analyzer/tests/pass-only/types/keyof/3.ts b/crates/stc_ts_file_analyzer/tests/pass-only/types/keyof/3.ts new file mode 100644 index 0000000000..968518c5d3 --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/pass-only/types/keyof/3.ts @@ -0,0 +1,19 @@ +// @strictNullChecks: true +// @declaration: true + +class Shape { + name: string; + width: number; + height: number; + visible: boolean; +} + +function getProperty(obj: T, key: K) { + return obj[key]; +} + +function f33(shape: S, key: K) { + let name = getProperty(shape, "name"); + let prop = getProperty(shape, key); + return prop; +} \ No newline at end of file diff --git a/crates/stc_ts_file_analyzer/tests/pass-only/types/mapped/.1.ts b/crates/stc_ts_file_analyzer/tests/pass-only/types/mapped/.1.ts new file mode 100644 index 0000000000..0a612c6dee --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/pass-only/types/mapped/.1.ts @@ -0,0 +1,6 @@ +// @strictNullChecks: true +// @declaration: true + +function f4(x: T, y: U, k: K) { + x[k] = y[k]; +} diff --git a/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.ts b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.ts new file mode 100644 index 0000000000..6d6531b87c --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.ts @@ -0,0 +1,23 @@ +// @strictNullChecks: true +// @declaration: true + +function f1(x: T, k: keyof T) { + return x[k]; +} + +function f2(x: T, k: K) { + return x[k]; +} + +function f3(x: T, y: U, k: keyof T) { + x[k] = y[k]; +} + +function f10(x: T, y: Partial, k: keyof T) { + y[k] = x[k]; +} + +function f12(x: T, y: Partial, k: keyof T) { + x[k] = y[k]; // Error + y[k] = x[k]; // Error +} diff --git a/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.tsc-errors.json b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.tsc-errors.json new file mode 100644 index 0000000000..0622117749 --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/1.tsc-errors.json @@ -0,0 +1,14 @@ +[ + { + "file": "tests/tsc/types/mapped/1.ts", + "line": 21, + "col": 5, + "code": 2322 + }, + { + "file": "tests/tsc/types/mapped/1.ts", + "line": 22, + "col": 5, + "code": 2322 + } +] \ No newline at end of file diff --git a/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.ts b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.ts new file mode 100644 index 0000000000..df29528d6d --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.ts @@ -0,0 +1,7 @@ +// @strictNullChecks: true +// @declaration: true + +function f6(x: T, y: U, k: K) { + x[k] = y[k]; // Error + y[k] = x[k]; // Error +} diff --git a/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.tsc-errors.json b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.tsc-errors.json new file mode 100644 index 0000000000..27c9cfd682 --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/tsc/types/mapped/2.tsc-errors.json @@ -0,0 +1,14 @@ +[ + { + "file": "tests/tsc/types/mapped/2.ts", + "line": 5, + "col": 5, + "code": 2536 + }, + { + "file": "tests/tsc/types/mapped/2.ts", + "line": 6, + "col": 12, + "code": 2536 + } +] \ No newline at end of file diff --git a/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/.1.ts b/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.ts similarity index 68% rename from crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/.1.ts rename to crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.ts index 37063f079b..3bae34a150 100644 --- a/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/.1.ts +++ b/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.ts @@ -6,4 +6,7 @@ function l(s: string, tp: T[P]): void { } function m(s: string, tp: T[P]): void { tp = s; -} \ No newline at end of file +} +function f(s: string, tp: T[P]): void { + tp = s; +} diff --git a/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.tsc-errors.json b/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.tsc-errors.json new file mode 100644 index 0000000000..a05a87d41a --- /dev/null +++ b/crates/stc_ts_file_analyzer/tests/tsc/types/nonPrimitive/1.tsc-errors.json @@ -0,0 +1,20 @@ +[ + { + "file": "tests/tsc/types/nonPrimitive/1.ts", + "line": 5, + "col": 5, + "code": 2322 + }, + { + "file": "tests/tsc/types/nonPrimitive/1.ts", + "line": 8, + "col": 5, + "code": 2322 + }, + { + "file": "tests/tsc/types/nonPrimitive/1.ts", + "line": 11, + "col": 5, + "code": 2322 + } +] \ No newline at end of file diff --git a/crates/stc_ts_type_checker/tests/conformance.pass.txt b/crates/stc_ts_type_checker/tests/conformance.pass.txt index 2e71241343..58892e1825 100644 --- a/crates/stc_ts_type_checker/tests/conformance.pass.txt +++ b/crates/stc_ts_type_checker/tests/conformance.pass.txt @@ -2428,6 +2428,7 @@ types/nonPrimitive/nonPrimitiveAndEmptyObject.ts types/nonPrimitive/nonPrimitiveAndTypeVariables.ts types/nonPrimitive/nonPrimitiveAsProperty.ts types/nonPrimitive/nonPrimitiveAssignError.ts +types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.ts types/nonPrimitive/nonPrimitiveInFunction.ts types/nonPrimitive/nonPrimitiveInGeneric.ts types/nonPrimitive/nonPrimitiveIndexingWithForIn.ts diff --git a/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.error-diff.json b/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.error-diff.json index e0ebb4a483..c9599c2010 100644 --- a/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.error-diff.json +++ b/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.error-diff.json @@ -1,13 +1,16 @@ { "required_errors": { - "TS2322": 12, - "TS2536": 4, - "TS2542": 4 + "TS2542": 4, + "TS2322": 10 }, "required_error_lines": { + "TS2542": [ + 54, + 59, + 64, + 69 + ], "TS2322": [ - 14, - 19, 75, 81, 91, @@ -18,25 +21,14 @@ 161, 166, 171 - ], - "TS2536": [ - 23, - 24, - 28, - 29 - ], - "TS2542": [ - 54, - 59, - 64, - 69 ] }, "extra_errors": { - "TS2322": 5 + "TS2322": 6 }, "extra_error_lines": { "TS2322": [ + 18, 39, 58, 59, diff --git a/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.stats.rust-debug b/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.stats.rust-debug index 4e774fdd28..7739abdef4 100644 --- a/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.stats.rust-debug +++ b/crates/stc_ts_type_checker/tests/conformance/types/mapped/mappedTypeRelationships.stats.rust-debug @@ -1,6 +1,6 @@ Stats { - required_error: 20, - matched_error: 8, - extra_error: 5, + required_error: 14, + matched_error: 14, + extra_error: 6, panic: 0, } \ No newline at end of file diff --git a/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.error-diff.json b/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.error-diff.json deleted file mode 100644 index b6be05c17d..0000000000 --- a/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.error-diff.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "required_errors": { - "TS2322": 1 - }, - "required_error_lines": { - "TS2322": [ - 28 - ] - }, - "extra_errors": {}, - "extra_error_lines": {} -} \ No newline at end of file diff --git a/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.stats.rust-debug b/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.stats.rust-debug index 4074730ba8..5fd5d2127d 100644 --- a/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.stats.rust-debug +++ b/crates/stc_ts_type_checker/tests/conformance/types/nonPrimitive/nonPrimitiveConstraintOfIndexAccessType.stats.rust-debug @@ -1,6 +1,6 @@ Stats { - required_error: 1, - matched_error: 9, + required_error: 0, + matched_error: 10, extra_error: 0, panic: 0, } \ No newline at end of file diff --git a/crates/stc_ts_type_checker/tests/tsc-stats.rust-debug b/crates/stc_ts_type_checker/tests/tsc-stats.rust-debug index 8ce4c1a1a4..26769c1753 100644 --- a/crates/stc_ts_type_checker/tests/tsc-stats.rust-debug +++ b/crates/stc_ts_type_checker/tests/tsc-stats.rust-debug @@ -1,6 +1,6 @@ Stats { - required_error: 3500, - matched_error: 6535, - extra_error: 763, + required_error: 3493, + matched_error: 6542, + extra_error: 764, panic: 73, } \ No newline at end of file