From 1f6b1931c2cc06271a1cbd40aed105f3d81de62c Mon Sep 17 00:00:00 2001 From: Boshen Date: Tue, 3 Feb 2026 14:47:05 +0000 Subject: [PATCH] fix(parser): validate TypeScript import type options (#18889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add validation for TypeScript import type options (`typeof import("...", { with: {...} })`) - Require `with` or `assert` keyword as identifier (not string, not escaped) - Reject computed property keys in attributes object - Reject spread elements in attributes object This fixes 4 Babel conformance tests: - `invalid-import-type-options-escaped-with` - `invalid-import-type-options-string-with` - `invalid-import-type-options-with-computed-properties` - `invalid-import-type-options-with-spread-element` **parser_babel Negative Passed**: 1655/1689 โ†’ 1659/1689 (+4) ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) --- crates/oxc_parser/src/diagnostics.rs | 16 ++ crates/oxc_parser/src/ts/types.rs | 142 +++++++++++++++++- tasks/coverage/snapshots/parser_babel.snap | 40 +++-- tasks/coverage/snapshots/parser_misc.snap | 32 ++++ .../coverage/snapshots/parser_typescript.snap | 87 ++++++++--- 5 files changed, 285 insertions(+), 32 deletions(-) diff --git a/crates/oxc_parser/src/diagnostics.rs b/crates/oxc_parser/src/diagnostics.rs index a4a1f76f37db1..5abf5e2577717 100644 --- a/crates/oxc_parser/src/diagnostics.rs +++ b/crates/oxc_parser/src/diagnostics.rs @@ -1268,3 +1268,19 @@ pub fn only_default_import_allowed_in_source_phase(span: Span) -> OxcDiagnostic OxcDiagnostic::error("Only a single default import is allowed in a source phase import.") .with_label(span) } + +#[cold] +pub fn ts_import_type_options_expected_with(span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("Expected 'with' in import type options").with_label(span) +} + +#[cold] +pub fn ts_import_type_options_invalid_key(span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("Import attributes keys must be identifier or string literal.") + .with_label(span) +} + +#[cold] +pub fn ts_import_type_options_no_spread(span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("Spread elements are not allowed in import type options.").with_label(span) +} diff --git a/crates/oxc_parser/src/ts/types.rs b/crates/oxc_parser/src/ts/types.rs index 1b60f2e098c58..e9a5652a4c28d 100644 --- a/crates/oxc_parser/src/ts/types.rs +++ b/crates/oxc_parser/src/ts/types.rs @@ -1075,7 +1075,7 @@ impl<'a> ParserImpl<'a> { }; let options = - if self.eat(Kind::Comma) { Some(self.parse_object_expression()) } else { None }; + if self.eat(Kind::Comma) { Some(self.parse_ts_import_type_options()) } else { None }; self.expect(Kind::RParen); let qualifier = if self.eat(Kind::Dot) { Some(self.parse_ts_import_type_qualifier()) } else { None }; @@ -1103,6 +1103,146 @@ impl<'a> ParserImpl<'a> { left } + /// Parse TypeScript import type options: `{ with: { type: "json" } }` or `{ assert: { ... } }` + /// + /// The options must have a property with key `with` or `assert` (as identifier, not string). + /// If the value is an object literal, it must have only static key-value pairs + /// (no computed keys, no spread elements). + fn parse_ts_import_type_options(&mut self) -> Box<'a, ObjectExpression<'a>> { + let span = self.start_span(); + self.expect(Kind::LCurly); + + // Expect `with` or `assert` as identifier (not string, not escaped) + // TypeScript supports both: `with` is the current standard, `assert` is the older syntax + let key_span = self.cur_token().span(); + let is_with = self.at(Kind::With); + let is_assert = self.at(Kind::Assert); + if (!is_with && !is_assert) || self.cur_token().escaped() { + self.error(diagnostics::ts_import_type_options_expected_with(key_span)); + } + // Use the actual string from the source (not a static string) to ensure it's in the arena + let key_name = self.cur_string(); + let with_key_span = self.start_span(); + self.bump_any(); + let with_key = self.ast.identifier_name(self.end_span(with_key_span), key_name); + + self.expect(Kind::Colon); + + // Parse the value - if it's an object literal, validate it + let value = if self.at(Kind::LCurly) { + let inner_object = self.parse_ts_import_type_attributes(); + Expression::ObjectExpression(self.alloc(inner_object)) + } else { + // Allow any expression (e.g., super.foo) + self.parse_assignment_expression_or_higher() + }; + + // Create the outer `with: { ... }` property + let with_property = self.ast.alloc_object_property( + self.end_span(with_key_span), + PropertyKind::Init, + PropertyKey::StaticIdentifier(self.alloc(with_key)), + value, + false, + false, + false, + ); + + let outer_properties = self.ast.vec1(ObjectPropertyKind::ObjectProperty(with_property)); + + // Allow optional trailing comma: `{ with: { type: "json" }, }` + let _ = self.eat(Kind::Comma); + + self.expect(Kind::RCurly); + self.ast.alloc_object_expression(self.end_span(span), outer_properties) + } + + /// Parse TypeScript import type attributes object: `{ type: "json" }` + /// Only allows static key-value pairs (no computed keys, no spread elements). + fn parse_ts_import_type_attributes(&mut self) -> ObjectExpression<'a> { + let span = self.start_span(); + self.expect(Kind::LCurly); + + let mut properties = self.ast.vec(); + let mut first = true; + while !self.at(Kind::RCurly) && !self.at(Kind::Eof) { + if first { + first = false; + } else { + self.expect(Kind::Comma); + if self.at(Kind::RCurly) { + break; + } + } + + // Check for spread element + if self.at(Kind::Dot3) { + let spread_span = self.cur_token().span(); + self.error(diagnostics::ts_import_type_options_no_spread(spread_span)); + // Skip the spread and parse the expression to recover + self.bump_any(); + self.parse_assignment_expression_or_higher(); + continue; + } + + let prop_span = self.start_span(); + + // Check for computed property + if self.at(Kind::LBrack) { + let bracket_span = self.cur_token().span(); + self.error(diagnostics::ts_import_type_options_invalid_key(bracket_span)); + // Parse as computed to recover + self.bump_any(); + self.parse_assignment_expression_or_higher(); + self.expect(Kind::RBrack); + self.expect(Kind::Colon); + let value = self.parse_assignment_expression_or_higher(); + let key = PropertyKey::StringLiteral(self.alloc(self.ast.string_literal( + bracket_span, + "", + None, + ))); + properties.push(ObjectPropertyKind::ObjectProperty( + self.ast.alloc_object_property( + self.end_span(prop_span), + PropertyKind::Init, + key, + value, + false, + false, + true, // computed + ), + )); + continue; + } + + // Parse identifier or string key + let key = if self.at(Kind::Str) { + let string_literal = self.parse_literal_string(); + PropertyKey::StringLiteral(self.alloc(string_literal)) + } else { + let ident = self.parse_identifier_name(); + PropertyKey::StaticIdentifier(self.alloc(ident)) + }; + + self.expect(Kind::Colon); + let value = self.parse_assignment_expression_or_higher(); + + properties.push(ObjectPropertyKind::ObjectProperty(self.ast.alloc_object_property( + self.end_span(prop_span), + PropertyKind::Init, + key, + value, + false, + false, + false, + ))); + } + + self.expect(Kind::RCurly); + self.ast.object_expression(self.end_span(span), properties) + } + fn try_parse_constraint_of_infer_type(&mut self) -> Option> { if self.eat(Kind::Extends) { let constraint = diff --git a/tasks/coverage/snapshots/parser_babel.snap b/tasks/coverage/snapshots/parser_babel.snap index f60a78accd4d3..0478e42eed829 100644 --- a/tasks/coverage/snapshots/parser_babel.snap +++ b/tasks/coverage/snapshots/parser_babel.snap @@ -3,7 +3,7 @@ commit: 92c052dc parser_babel Summary: AST Parsed : 2224/2224 (100.00%) Positive Passed: 2211/2224 (99.42%) -Negative Passed: 1656/1689 (98.05%) +Negative Passed: 1660/1689 (98.28%) Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2026/explicit-resource-management/invalid-for-using-of-no-initializer/input.js Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/cast/unparenthesized-assert-and-assign/input.ts @@ -62,14 +62,6 @@ Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/ty Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/const-type-parameters-invalid/input.ts -Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-escaped-with/input.ts - -Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-string-with/input.ts - -Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-computed-properties/input.ts - -Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-spread-element/input.ts - Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/estree/class-private-method/typescript-invalid-abstract/input.ts ร— TS(18019): 'abstract' modifier cannot be used with a private identifier. @@ -14204,6 +14196,24 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc ยท โ•ฐโ”€โ”€ `{` expected โ•ฐโ”€โ”€โ”€โ”€ + ร— Expected 'with' in import type options + โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-escaped-with/input.ts:1:36] + 1 โ”‚ let x: typeof import("foo.json", { w\u0069th: { type: "json" } }); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Keywords cannot contain escape characters + โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-escaped-with/input.ts:1:36] + 1 โ”‚ let x: typeof import("foo.json", { w\u0069th: { type: "json" } }); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-string-with/input.ts:1:36] + 1 โ”‚ let x: typeof import("foo.json", { "with": { type: "json" } }); + ยท โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + ร— Expected `)` but found `,` โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-trailing-comma/input.ts:1:60] 1 โ”‚ let x: typeof import("foo.json", { with: { type: "json" } }, ) @@ -14211,6 +14221,18 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc ยท โ•ฐโ”€โ”€ `)` expected โ•ฐโ”€โ”€โ”€โ”€ + ร— Import attributes keys must be identifier or string literal. + โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-computed-properties/input.ts:1:44] + 1 โ”‚ let x: typeof import("foo.json", { with: { ["type"]: "json" } }); + ยท โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Spread elements are not allowed in import type options. + โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-spread-element/input.ts:1:58] + 1 โ”‚ let x: typeof import("foo.json", { with: { type: "json", ...attributes } }); + ยท โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + ร— Expected `{` but found `)` โ•ญโ”€[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-trailing-comma/input.ts:1:34] 1 โ”‚ let x: typeof import("foo.json", ) diff --git a/tasks/coverage/snapshots/parser_misc.snap b/tasks/coverage/snapshots/parser_misc.snap index b5b3e012711b5..5ffb312ff62fa 100644 --- a/tasks/coverage/snapshots/parser_misc.snap +++ b/tasks/coverage/snapshots/parser_misc.snap @@ -3534,6 +3534,38 @@ Negative Passed: 134/134 (100.00%) ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ•ฐโ”€โ”€โ”€โ”€ + ร— Expected 'with' in import type options + โ•ญโ”€[misc/fail/oxc-2394.ts:20:22] + 19 โ”‚ export type LocalInterface = + 20 โ”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 21 โ”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface; + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[misc/fail/oxc-2394.ts:21:22] + 20 โ”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface + 21 โ”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface; + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 22 โ”‚ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[misc/fail/oxc-2394.ts:23:49] + 22 โ”‚ + 23 โ”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 24 โ”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface); + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[misc/fail/oxc-2394.ts:24:49] + 23 โ”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface); + 24 โ”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 25 โ”‚ + โ•ฐโ”€โ”€โ”€โ”€ + ร— Expected `{` but found `[` โ•ญโ”€[misc/fail/oxc-2394.ts:38:21] 37 โ”‚ export type LocalInterface = diff --git a/tasks/coverage/snapshots/parser_typescript.snap b/tasks/coverage/snapshots/parser_typescript.snap index 7203d7d8ac15b..b2fab81612855 100644 --- a/tasks/coverage/snapshots/parser_typescript.snap +++ b/tasks/coverage/snapshots/parser_typescript.snap @@ -9189,12 +9189,11 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va ยท โ”€ โ•ฐโ”€โ”€โ”€โ”€ - ร— Expected `:` but found `,` - โ•ญโ”€[typescript/tests/cases/compiler/parseAssertEntriesError.ts:2:36] + ร— Unexpected token + โ•ญโ”€[typescript/tests/cases/compiler/parseAssertEntriesError.ts:2:32] 1 โ”‚ export type LocalInterface = 2 โ”‚ & import("pkg", { assert: {1234, "resolution-mode": "require"} }).RequireInterface - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ `:` expected + ยท โ”€โ”€โ”€โ”€ 3 โ”‚ & import("pkg", { assert: {1234, "resolution-mode": "import"} }).ImportInterface; โ•ฐโ”€โ”€โ”€โ”€ @@ -9236,12 +9235,11 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va 3 โ”‚ } โ•ฐโ”€โ”€โ”€โ”€ - ร— Expected `:` but found `,` - โ•ญโ”€[typescript/tests/cases/compiler/parseImportAttributesError.ts:2:34] + ร— Unexpected token + โ•ญโ”€[typescript/tests/cases/compiler/parseImportAttributesError.ts:2:30] 1 โ”‚ export type LocalInterface = 2 โ”‚ & import("pkg", { with: {1234, "resolution-mode": "require"} }).RequireInterface - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ `:` expected + ยท โ”€โ”€โ”€โ”€ 3 โ”‚ & import("pkg", { with: {1234, "resolution-mode": "import"} }).ImportInterface; โ•ฐโ”€โ”€โ”€โ”€ @@ -20199,11 +20197,12 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va 9 โ”‚ const g = import('./0', {}, {}) โ•ฐโ”€โ”€โ”€โ”€ - ร— Unexpected token + ร— Expected `}` but found `,` โ•ญโ”€[typescript/tests/cases/conformance/importAttributes/importAttributes10.ts:22:5] 21 โ”‚ type: "json" 22 โ”‚ },, - ยท โ”€ + ยท โ”ฌ + ยท โ•ฐโ”€โ”€ `}` expected 23 โ”‚ }); โ•ฐโ”€โ”€โ”€โ”€ @@ -21116,22 +21115,66 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va โ•ฐโ”€โ”€โ”€โ”€ help: TypeScript transforms 'import ... =' to 'const ... =' - ร— Expected `{` but found `[` - โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:3:21] + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:3:22] 2 โ”‚ export type LocalInterface = - 3 โ”‚ & import("pkg", [ {"resolution-mode": "require"} ]).RequireInterface - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ `{` expected - 4 โ”‚ & import("pkg", [ {"resolution-mode": "import"} ]).ImportInterface; + 3 โ”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 4 โ”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface; + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:4:22] + 3 โ”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface + 4 โ”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface; + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 5 โ”‚ โ•ฐโ”€โ”€โ”€โ”€ - ร— Expected `{` but found `[` - โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:3:21] + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:6:49] + 5 โ”‚ + 6 โ”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 7 โ”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface); + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:7:49] + 6 โ”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface); + 7 โ”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:3:22] 2 โ”‚ export type LocalInterface = - 3 โ”‚ & import("pkg", [ {"resolution-mode": "require"} ]).RequireInterface - ยท โ”ฌ - ยท โ•ฐโ”€โ”€ `{` expected - 4 โ”‚ & import("pkg", [ {"resolution-mode": "import"} ]).ImportInterface; + 3 โ”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 4 โ”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface; + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:4:22] + 3 โ”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface + 4 โ”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface; + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 5 โ”‚ + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:6:49] + 5 โ”‚ + 6 โ”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 7 โ”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface); + โ•ฐโ”€โ”€โ”€โ”€ + + ร— Expected 'with' in import type options + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:7:49] + 6 โ”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface); + 7 โ”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ•ฐโ”€โ”€โ”€โ”€ ร— TS(1030): 'override' modifier already seen.