Skip to content

Commit 38e717a

Browse files
Jack-Worksorta
andauthored
fix: hyphened name not auto-completed by the ls (microsoft#37455)
* fix: hyphened name not auto-completed by the ls * fix: accept new baseline * Adds a test to validate the hypened identifiers in JSX Co-authored-by: Orta Therox <[email protected]>
1 parent 221a2ae commit 38e717a

File tree

5 files changed

+60
-37
lines changed

5 files changed

+60
-37
lines changed

src/compiler/scanner.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -309,14 +309,14 @@ namespace ts {
309309
return languageVersion! >= ScriptTarget.ES2015 ?
310310
lookupInUnicodeMap(code, unicodeESNextIdentifierStart) :
311311
languageVersion! === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) :
312-
lookupInUnicodeMap(code, unicodeES3IdentifierStart);
312+
lookupInUnicodeMap(code, unicodeES3IdentifierStart);
313313
}
314314

315315
function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) {
316316
return languageVersion! >= ScriptTarget.ES2015 ?
317317
lookupInUnicodeMap(code, unicodeESNextIdentifierPart) :
318318
languageVersion! === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) :
319-
lookupInUnicodeMap(code, unicodeES3IdentifierPart);
319+
lookupInUnicodeMap(code, unicodeES3IdentifierPart);
320320
}
321321

322322
function makeReverseMap(source: Map<number>): string[] {
@@ -350,7 +350,7 @@ namespace ts {
350350
if (text.charCodeAt(pos) === CharacterCodes.lineFeed) {
351351
pos++;
352352
}
353-
// falls through
353+
// falls through
354354
case CharacterCodes.lineFeed:
355355
result.push(lineStart);
356356
lineStart = pos;
@@ -522,8 +522,8 @@ namespace ts {
522522
case CharacterCodes.formFeed:
523523
case CharacterCodes.space:
524524
case CharacterCodes.slash:
525-
// starts of normal trivia
526-
// falls through
525+
// starts of normal trivia
526+
// falls through
527527
case CharacterCodes.lessThan:
528528
case CharacterCodes.bar:
529529
case CharacterCodes.equals:
@@ -552,7 +552,7 @@ namespace ts {
552552
if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) {
553553
pos++;
554554
}
555-
// falls through
555+
// falls through
556556
case CharacterCodes.lineFeed:
557557
pos++;
558558
if (stopAfterLineBreak) {
@@ -734,7 +734,7 @@ namespace ts {
734734
if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) {
735735
pos++;
736736
}
737-
// falls through
737+
// falls through
738738
case CharacterCodes.lineFeed:
739739
pos++;
740740
if (trailing) {
@@ -868,21 +868,23 @@ namespace ts {
868868
ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch, languageVersion);
869869
}
870870

871-
export function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean {
871+
export function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean {
872872
return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z ||
873873
ch >= CharacterCodes._0 && ch <= CharacterCodes._9 || ch === CharacterCodes.$ || ch === CharacterCodes._ ||
874+
// "-" and ":" are valid in JSX Identifiers
875+
(identifierVariant === LanguageVariant.JSX ? (ch === CharacterCodes.minus || ch === CharacterCodes.colon) : false) ||
874876
ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion);
875877
}
876878

877879
/* @internal */
878-
export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined): boolean {
880+
export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean {
879881
let ch = codePointAt(name, 0);
880882
if (!isIdentifierStart(ch, languageVersion)) {
881883
return false;
882884
}
883885

884886
for (let i = charSize(ch); i < name.length; i += charSize(ch)) {
885-
if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion)) {
887+
if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion, identifierVariant)) {
886888
return false;
887889
}
888890
}
@@ -1024,7 +1026,7 @@ namespace ts {
10241026
return result + text.substring(start, pos);
10251027
}
10261028

1027-
function scanNumber(): {type: SyntaxKind, value: string} {
1029+
function scanNumber(): { type: SyntaxKind, value: string } {
10281030
const start = pos;
10291031
const mainFragment = scanNumberFragment();
10301032
let decimalFragment: string | undefined;
@@ -1371,7 +1373,7 @@ namespace ts {
13711373
if (pos < end && text.charCodeAt(pos) === CharacterCodes.lineFeed) {
13721374
pos++;
13731375
}
1374-
// falls through
1376+
// falls through
13751377
case CharacterCodes.lineFeed:
13761378
case CharacterCodes.lineSeparator:
13771379
case CharacterCodes.paragraphSeparator:
@@ -1837,10 +1839,10 @@ namespace ts {
18371839
tokenFlags |= TokenFlags.Octal;
18381840
return token = SyntaxKind.NumericLiteral;
18391841
}
1840-
// This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero
1841-
// can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being
1842-
// permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do).
1843-
// falls through
1842+
// This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero
1843+
// can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being
1844+
// permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do).
1845+
// falls through
18441846
case CharacterCodes._1:
18451847
case CharacterCodes._2:
18461848
case CharacterCodes._3:
@@ -1879,8 +1881,8 @@ namespace ts {
18791881
return pos += 2, token = SyntaxKind.LessThanEqualsToken;
18801882
}
18811883
if (languageVariant === LanguageVariant.JSX &&
1882-
text.charCodeAt(pos + 1) === CharacterCodes.slash &&
1883-
text.charCodeAt(pos + 2) !== CharacterCodes.asterisk) {
1884+
text.charCodeAt(pos + 1) === CharacterCodes.slash &&
1885+
text.charCodeAt(pos + 2) !== CharacterCodes.asterisk) {
18841886
return pos += 2, token = SyntaxKind.LessThanSlashToken;
18851887
}
18861888
pos++;

src/services/completions.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ namespace ts.Completions {
232232
completionKind,
233233
preferences,
234234
propertyAccessToConvert,
235+
completionData.isJsxIdentifierExpected,
235236
isJsxInitializer,
236237
recommendedCompletion,
237238
symbolToOriginInfoMap,
@@ -255,6 +256,7 @@ namespace ts.Completions {
255256
completionKind,
256257
preferences,
257258
propertyAccessToConvert,
259+
completionData.isJsxIdentifierExpected,
258260
isJsxInitializer,
259261
recommendedCompletion,
260262
symbolToOriginInfoMap,
@@ -441,6 +443,7 @@ namespace ts.Completions {
441443
kind: CompletionKind,
442444
preferences: UserPreferences,
443445
propertyAccessToConvert?: PropertyAccessExpression,
446+
jsxIdentifierExpected?: boolean,
444447
isJsxInitializer?: IsJsxInitializer,
445448
recommendedCompletion?: Symbol,
446449
symbolToOriginInfoMap?: SymbolOriginInfoMap,
@@ -454,7 +457,7 @@ namespace ts.Completions {
454457
const uniques = createMap<true>();
455458
for (const symbol of symbols) {
456459
const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined;
457-
const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind);
460+
const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected);
458461
if (!info) {
459462
continue;
460463
}
@@ -564,7 +567,7 @@ namespace ts.Completions {
564567
// completion entry.
565568
return firstDefined(symbols, (symbol): SymbolCompletion | undefined => {
566569
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
567-
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind);
570+
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind, completionData.isJsxIdentifierExpected);
568571
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
569572
? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation }
570573
: undefined;
@@ -716,6 +719,8 @@ namespace ts.Completions {
716719
readonly insideJsDocTagTypeExpression: boolean;
717720
readonly symbolToSortTextMap: SymbolSortTextMap;
718721
readonly isTypeOnlyLocation: boolean;
722+
/** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */
723+
readonly isJsxIdentifierExpected: boolean;
719724
}
720725
type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag };
721726

@@ -895,6 +900,7 @@ namespace ts.Completions {
895900
let isRightOfOpenTag = false;
896901
let isStartingCloseTag = false;
897902
let isJsxInitializer: IsJsxInitializer = false;
903+
let isJsxIdentifierExpected = false;
898904

899905
let location = getTouchingPropertyName(sourceFile, position);
900906
if (contextToken) {
@@ -975,11 +981,12 @@ namespace ts.Completions {
975981
if (!binaryExpressionMayBeOpenTag(parent as BinaryExpression)) {
976982
break;
977983
}
978-
// falls through
984+
// falls through
979985

980986
case SyntaxKind.JsxSelfClosingElement:
981987
case SyntaxKind.JsxElement:
982988
case SyntaxKind.JsxOpeningElement:
989+
isJsxIdentifierExpected = true;
983990
if (contextToken.kind === SyntaxKind.LessThanToken) {
984991
isRightOfOpenTag = true;
985992
location = contextToken;
@@ -992,6 +999,7 @@ namespace ts.Completions {
992999
isJsxInitializer = true;
9931000
break;
9941001
case SyntaxKind.Identifier:
1002+
isJsxIdentifierExpected = true;
9951003
// For `<div x=[|f/**/|]`, `parent` will be `x` and `previousToken.parent` will be `f` (which is its own JsxAttribute)
9961004
// Note for `<div someBool f>` we don't want to treat this as a jsx inializer, instead it's the attribute name.
9971005
if (parent !== previousToken.parent &&
@@ -1066,7 +1074,8 @@ namespace ts.Completions {
10661074
isJsxInitializer,
10671075
insideJsDocTagTypeExpression,
10681076
symbolToSortTextMap,
1069-
isTypeOnlyLocation: isTypeOnly
1077+
isTypeOnlyLocation: isTypeOnly,
1078+
isJsxIdentifierExpected,
10701079
};
10711080

10721081
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
@@ -1130,9 +1139,9 @@ namespace ts.Completions {
11301139
let insertQuestionDot = false;
11311140
if (type.isNullableType()) {
11321141
const canCorrectToQuestionDot =
1133-
isRightOfDot &&
1134-
!isRightOfQuestionDot &&
1135-
preferences.includeAutomaticOptionalChainCompletions !== false;
1142+
isRightOfDot &&
1143+
!isRightOfQuestionDot &&
1144+
preferences.includeAutomaticOptionalChainCompletions !== false;
11361145

11371146
if (canCorrectToQuestionDot || isRightOfQuestionDot) {
11381147
type = type.getNonNullableType();
@@ -1447,8 +1456,8 @@ namespace ts.Completions {
14471456
return insideJsDocTagTypeExpression
14481457
|| !isContextTokenValueLocation(contextToken) &&
14491458
(isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker)
1450-
|| isPartOfTypeNode(location)
1451-
|| isContextTokenTypeLocation(contextToken));
1459+
|| isPartOfTypeNode(location)
1460+
|| isContextTokenTypeLocation(contextToken));
14521461
}
14531462

14541463
function isContextTokenValueLocation(contextToken: Node) {
@@ -2399,6 +2408,7 @@ namespace ts.Completions {
23992408
target: ScriptTarget,
24002409
origin: SymbolOriginInfo | undefined,
24012410
kind: CompletionKind,
2411+
jsxIdentifierExpected: boolean,
24022412
): CompletionEntryDisplayNameForSymbol | undefined {
24032413
const name = originIsExport(origin) ? getNameForExportedSymbol(symbol, target) : symbol.name;
24042414
if (name === undefined
@@ -2411,7 +2421,7 @@ namespace ts.Completions {
24112421
}
24122422

24132423
const validNameResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false };
2414-
if (isIdentifierText(name, target) || symbol.valueDeclaration && isPrivateIdentifierPropertyDeclaration(symbol.valueDeclaration)) {
2424+
if (isIdentifierText(name, target, jsxIdentifierExpected ? LanguageVariant.JSX : LanguageVariant.Standard) || symbol.valueDeclaration && isPrivateIdentifierPropertyDeclaration(symbol.valueDeclaration)) {
24152425
return validNameResult;
24162426
}
24172427
switch (kind) {

tests/baselines/reference/api/tsserverlibrary.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3399,7 +3399,7 @@ declare namespace ts {
33993399
/** Optionally, get the shebang */
34003400
function getShebang(text: string): string | undefined;
34013401
function isIdentifierStart(ch: number, languageVersion: ScriptTarget | undefined): boolean;
3402-
function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean;
3402+
function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean;
34033403
function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean, languageVariant?: LanguageVariant, textInitial?: string, onError?: ErrorCallback, start?: number, length?: number): Scanner;
34043404
}
34053405
declare namespace ts {

tests/baselines/reference/api/typescript.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3399,7 +3399,7 @@ declare namespace ts {
33993399
/** Optionally, get the shebang */
34003400
function getShebang(text: string): string | undefined;
34013401
function isIdentifierStart(ch: number, languageVersion: ScriptTarget | undefined): boolean;
3402-
function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean;
3402+
function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean;
34033403
function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean, languageVariant?: LanguageVariant, textInitial?: string, onError?: ErrorCallback, start?: number, length?: number): Scanner;
34043404
}
34053405
declare namespace ts {

tests/cases/fourslash/completionsInJsxTag.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
//// div: {
1010
//// /** Doc */
1111
//// foo: string
12+
//// /** Label docs */
13+
//// "aria-label": string
1214
//// }
1315
//// }
1416
////}
@@ -21,11 +23,20 @@
2123

2224
verify.completions({
2325
marker: ["1", "2"],
24-
exact: {
25-
name: "foo",
26-
text: "(JSX attribute) foo: string",
27-
documentation: "Doc",
28-
kind: "JSX attribute",
29-
kindModifiers: "declare",
30-
},
26+
exact: [
27+
{
28+
name: "foo",
29+
text: "(JSX attribute) foo: string",
30+
documentation: "Doc",
31+
kind: "JSX attribute",
32+
kindModifiers: "declare",
33+
},
34+
{
35+
name: "aria-label",
36+
text: "(JSX attribute) \"aria-label\": string",
37+
documentation: "Label docs",
38+
kind: "JSX attribute",
39+
kindModifiers: "declare",
40+
},
41+
],
3142
});

0 commit comments

Comments
 (0)