diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index ab08af376d1bb..6def169816949 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -308,14 +308,14 @@ namespace ts { return languageVersion! >= ScriptTarget.ES2015 ? lookupInUnicodeMap(code, unicodeESNextIdentifierStart) : languageVersion! === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) : - lookupInUnicodeMap(code, unicodeES3IdentifierStart); + lookupInUnicodeMap(code, unicodeES3IdentifierStart); } function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) { return languageVersion! >= ScriptTarget.ES2015 ? lookupInUnicodeMap(code, unicodeESNextIdentifierPart) : languageVersion! === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) : - lookupInUnicodeMap(code, unicodeES3IdentifierPart); + lookupInUnicodeMap(code, unicodeES3IdentifierPart); } function makeReverseMap(source: Map): string[] { @@ -349,7 +349,7 @@ namespace ts { if (text.charCodeAt(pos) === CharacterCodes.lineFeed) { pos++; } - // falls through + // falls through case CharacterCodes.lineFeed: result.push(lineStart); lineStart = pos; @@ -503,8 +503,8 @@ namespace ts { case CharacterCodes.formFeed: case CharacterCodes.space: case CharacterCodes.slash: - // starts of normal trivia - // falls through + // starts of normal trivia + // falls through case CharacterCodes.lessThan: case CharacterCodes.bar: case CharacterCodes.equals: @@ -533,7 +533,7 @@ namespace ts { if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { pos++; } - // falls through + // falls through case CharacterCodes.lineFeed: pos++; if (stopAfterLineBreak) { @@ -715,7 +715,7 @@ namespace ts { if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { pos++; } - // falls through + // falls through case CharacterCodes.lineFeed: pos++; if (trailing) { @@ -849,21 +849,23 @@ namespace ts { ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch, languageVersion); } - export function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean { + export function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean { return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z || ch >= CharacterCodes._0 && ch <= CharacterCodes._9 || ch === CharacterCodes.$ || ch === CharacterCodes._ || + // "-" and ":" are valid in JSX Identifiers + (identifierVariant === LanguageVariant.JSX ? (ch === CharacterCodes.minus || ch === CharacterCodes.colon) : false) || ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion); } /* @internal */ - export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined): boolean { + export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean { let ch = codePointAt(name, 0); if (!isIdentifierStart(ch, languageVersion)) { return false; } for (let i = charSize(ch); i < name.length; i += charSize(ch)) { - if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion)) { + if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion, identifierVariant)) { return false; } } @@ -1005,7 +1007,7 @@ namespace ts { return result + text.substring(start, pos); } - function scanNumber(): {type: SyntaxKind, value: string} { + function scanNumber(): { type: SyntaxKind, value: string } { const start = pos; const mainFragment = scanNumberFragment(); let decimalFragment: string | undefined; @@ -1352,7 +1354,7 @@ namespace ts { if (pos < end && text.charCodeAt(pos) === CharacterCodes.lineFeed) { pos++; } - // falls through + // falls through case CharacterCodes.lineFeed: case CharacterCodes.lineSeparator: case CharacterCodes.paragraphSeparator: @@ -1818,10 +1820,10 @@ namespace ts { tokenFlags |= TokenFlags.Octal; return token = SyntaxKind.NumericLiteral; } - // This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero - // can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being - // permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do). - // falls through + // This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero + // can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being + // permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do). + // falls through case CharacterCodes._1: case CharacterCodes._2: case CharacterCodes._3: @@ -1860,8 +1862,8 @@ namespace ts { return pos += 2, token = SyntaxKind.LessThanEqualsToken; } if (languageVariant === LanguageVariant.JSX && - text.charCodeAt(pos + 1) === CharacterCodes.slash && - text.charCodeAt(pos + 2) !== CharacterCodes.asterisk) { + text.charCodeAt(pos + 1) === CharacterCodes.slash && + text.charCodeAt(pos + 2) !== CharacterCodes.asterisk) { return pos += 2, token = SyntaxKind.LessThanSlashToken; } pos++; diff --git a/src/services/completions.ts b/src/services/completions.ts index 11f16ccf8fe2f..478a9ece1c691 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -232,6 +232,7 @@ namespace ts.Completions { completionKind, preferences, propertyAccessToConvert, + completionData.isJsxIdentifierExpected, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap, @@ -255,6 +256,7 @@ namespace ts.Completions { completionKind, preferences, propertyAccessToConvert, + completionData.isJsxIdentifierExpected, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap, @@ -441,6 +443,7 @@ namespace ts.Completions { kind: CompletionKind, preferences: UserPreferences, propertyAccessToConvert?: PropertyAccessExpression, + jsxIdentifierExpected?: boolean, isJsxInitializer?: IsJsxInitializer, recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, @@ -454,7 +457,7 @@ namespace ts.Completions { const uniques = createMap(); for (const symbol of symbols) { const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind); + const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); if (!info) { continue; } @@ -564,7 +567,7 @@ namespace ts.Completions { // completion entry. return firstDefined(symbols, (symbol): SymbolCompletion | undefined => { const origin = symbolToOriginInfoMap[getSymbolId(symbol)]; - const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind); + const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind, completionData.isJsxIdentifierExpected); return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source ? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } : undefined; @@ -716,6 +719,8 @@ namespace ts.Completions { readonly insideJsDocTagTypeExpression: boolean; readonly symbolToSortTextMap: SymbolSortTextMap; readonly isTypeOnlyLocation: boolean; + /** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */ + readonly isJsxIdentifierExpected: boolean; } type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }; @@ -895,6 +900,7 @@ namespace ts.Completions { let isRightOfOpenTag = false; let isStartingCloseTag = false; let isJsxInitializer: IsJsxInitializer = false; + let isJsxIdentifierExpected = false; let location = getTouchingPropertyName(sourceFile, position); if (contextToken) { @@ -975,11 +981,12 @@ namespace ts.Completions { if (!binaryExpressionMayBeOpenTag(parent as BinaryExpression)) { break; } - // falls through + // falls through case SyntaxKind.JsxSelfClosingElement: case SyntaxKind.JsxElement: case SyntaxKind.JsxOpeningElement: + isJsxIdentifierExpected = true; if (contextToken.kind === SyntaxKind.LessThanToken) { isRightOfOpenTag = true; location = contextToken; @@ -992,6 +999,7 @@ namespace ts.Completions { isJsxInitializer = true; break; case SyntaxKind.Identifier: + isJsxIdentifierExpected = true; // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. if (parent !== previousToken.parent && @@ -1066,7 +1074,8 @@ namespace ts.Completions { isJsxInitializer, insideJsDocTagTypeExpression, symbolToSortTextMap, - isTypeOnlyLocation: isTypeOnly + isTypeOnlyLocation: isTypeOnly, + isJsxIdentifierExpected, }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; @@ -1130,9 +1139,9 @@ namespace ts.Completions { let insertQuestionDot = false; if (type.isNullableType()) { const canCorrectToQuestionDot = - isRightOfDot && - !isRightOfQuestionDot && - preferences.includeAutomaticOptionalChainCompletions !== false; + isRightOfDot && + !isRightOfQuestionDot && + preferences.includeAutomaticOptionalChainCompletions !== false; if (canCorrectToQuestionDot || isRightOfQuestionDot) { type = type.getNonNullableType(); @@ -1447,8 +1456,8 @@ namespace ts.Completions { return insideJsDocTagTypeExpression || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker) - || isPartOfTypeNode(location) - || isContextTokenTypeLocation(contextToken)); + || isPartOfTypeNode(location) + || isContextTokenTypeLocation(contextToken)); } function isContextTokenValueLocation(contextToken: Node) { @@ -2399,6 +2408,7 @@ namespace ts.Completions { target: ScriptTarget, origin: SymbolOriginInfo | undefined, kind: CompletionKind, + jsxIdentifierExpected: boolean, ): CompletionEntryDisplayNameForSymbol | undefined { const name = originIsExport(origin) ? getNameForExportedSymbol(symbol, target) : symbol.name; if (name === undefined @@ -2411,7 +2421,7 @@ namespace ts.Completions { } const validNameResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; - if (isIdentifierText(name, target) || symbol.valueDeclaration && isPrivateIdentifierPropertyDeclaration(symbol.valueDeclaration)) { + if (isIdentifierText(name, target, jsxIdentifierExpected ? LanguageVariant.JSX : LanguageVariant.Standard) || symbol.valueDeclaration && isPrivateIdentifierPropertyDeclaration(symbol.valueDeclaration)) { return validNameResult; } switch (kind) { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5c0326d63ca24..e7261956e692a 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3394,7 +3394,7 @@ declare namespace ts { /** Optionally, get the shebang */ function getShebang(text: string): string | undefined; function isIdentifierStart(ch: number, languageVersion: ScriptTarget | undefined): boolean; - function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean; + function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean; function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean, languageVariant?: LanguageVariant, textInitial?: string, onError?: ErrorCallback, start?: number, length?: number): Scanner; } declare namespace ts { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 5bd7da4518176..27327212cf5d1 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3394,7 +3394,7 @@ declare namespace ts { /** Optionally, get the shebang */ function getShebang(text: string): string | undefined; function isIdentifierStart(ch: number, languageVersion: ScriptTarget | undefined): boolean; - function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean; + function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean; function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean, languageVariant?: LanguageVariant, textInitial?: string, onError?: ErrorCallback, start?: number, length?: number): Scanner; } declare namespace ts { diff --git a/tests/cases/fourslash/completionsInJsxTag.ts b/tests/cases/fourslash/completionsInJsxTag.ts index 1977333b62bce..e2fd2178cf54c 100644 --- a/tests/cases/fourslash/completionsInJsxTag.ts +++ b/tests/cases/fourslash/completionsInJsxTag.ts @@ -9,6 +9,8 @@ //// div: { //// /** Doc */ //// foo: string +//// /** Label docs */ +//// "aria-label": string //// } //// } ////} @@ -21,11 +23,20 @@ verify.completions({ marker: ["1", "2"], - exact: { - name: "foo", - text: "(JSX attribute) foo: string", - documentation: "Doc", - kind: "JSX attribute", - kindModifiers: "declare", - }, + exact: [ + { + name: "foo", + text: "(JSX attribute) foo: string", + documentation: "Doc", + kind: "JSX attribute", + kindModifiers: "declare", + }, + { + name: "aria-label", + text: "(JSX attribute) \"aria-label\": string", + documentation: "Label docs", + kind: "JSX attribute", + kindModifiers: "declare", + }, + ], });