Skip to content

Commit 8ace7b8

Browse files
author
Andy
authored
importFixes: Support missing "React" at a JSXOpeningElement (#16066)
* importFixes: Support missing "React" at a JSXOpeningElement * Fix nextLineRule linting * Rename to resolveJsxNamespaceAtLocation * Expose getJsxNamespace and resolveNameAtLocation separately
1 parent 7056411 commit 8ace7b8

12 files changed

+130
-21
lines changed

scripts/tslint/nextLineRule.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class Rule extends Lint.Rules.AbstractRule {
1818

1919
function walk(ctx: Lint.WalkContext<void>, checkCatch: boolean, checkElse: boolean): void {
2020
const { sourceFile } = ctx;
21-
function recur(node: ts.Node): void {
21+
ts.forEachChild(sourceFile, function recur(node) {
2222
switch (node.kind) {
2323
case ts.SyntaxKind.IfStatement:
2424
checkIf(node as ts.IfStatement);
@@ -28,7 +28,7 @@ function walk(ctx: Lint.WalkContext<void>, checkCatch: boolean, checkElse: boole
2828
break;
2929
}
3030
ts.forEachChild(node, recur);
31-
}
31+
});
3232

3333
function checkIf(node: ts.IfStatement): void {
3434
const { thenStatement, elseStatement } = node;

src/compiler/checker.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ namespace ts {
210210
getSuggestionForNonexistentProperty,
211211
getSuggestionForNonexistentSymbol,
212212
getBaseConstraintOfType,
213+
getJsxNamespace,
214+
resolveNameAtLocation(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined {
215+
location = getParseTreeNode(location);
216+
return resolveName(location, name, meaning, /*nameNotFoundMessage*/ undefined, name);
217+
},
213218
};
214219

215220
const tupleTypes: GenericType[] = [];
@@ -847,7 +852,7 @@ namespace ts {
847852
location: Node | undefined,
848853
name: string,
849854
meaning: SymbolFlags,
850-
nameNotFoundMessage: DiagnosticMessage,
855+
nameNotFoundMessage: DiagnosticMessage | undefined,
851856
nameArg: string | Identifier,
852857
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol {
853858
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, getSymbol, suggestedNameNotFoundMessage);
@@ -5841,7 +5846,8 @@ namespace ts {
58415846
}
58425847
}
58435848
return arrayFrom(props.values());
5844-
} else {
5849+
}
5850+
else {
58455851
return getPropertiesOfType(type);
58465852
}
58475853
}
@@ -14151,7 +14157,7 @@ namespace ts {
1415114157
checkJsxPreconditions(node);
1415214158
// The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import.
1415314159
// And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error.
14154-
const reactRefErr = compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
14160+
const reactRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
1415514161
const reactNamespace = getJsxNamespace();
1415614162
const reactSym = resolveName(node.tagName, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace);
1415714163
if (reactSym) {

src/compiler/program.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1479,7 +1479,8 @@ namespace ts {
14791479
}
14801480
}
14811481
return sourceFile;
1482-
} else {
1482+
}
1483+
else {
14831484
const sourceFileNoExtension = options.allowNonTsExtensions && getSourceFile(fileName);
14841485
if (sourceFileNoExtension) return sourceFileNoExtension;
14851486

src/compiler/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2605,6 +2605,9 @@ namespace ts {
26052605
* Does not include properties of primitive types.
26062606
*/
26072607
/* @internal */ getAllPossiblePropertiesOfType(type: Type): Symbol[];
2608+
2609+
/* @internal */ getJsxNamespace(): string;
2610+
/* @internal */ resolveNameAtLocation(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined;
26082611
}
26092612

26102613
export enum NodeBuilderFlags {

src/harness/fourslash.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -2372,7 +2372,10 @@ namespace FourSlash {
23722372
const codeFixes = this.getCodeFixActions(this.activeFile.fileName, errorCode);
23732373

23742374
if (!codeFixes || codeFixes.length === 0) {
2375-
this.raiseError("No codefixes returned.");
2375+
if (expectedTextArray.length !== 0) {
2376+
this.raiseError("No codefixes returned.");
2377+
}
2378+
return;
23762379
}
23772380

23782381
const actualTextArray: string[] = [];

src/harness/unittests/telemetry.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ namespace ts.projectSystem {
252252
}
253253

254254
getEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"], mayBeMore = false): T["data"] {
255-
if (mayBeMore) assert(this.events.length !== 0); else assert.equal(this.events.length, 1);
255+
if (mayBeMore) { assert(this.events.length !== 0); }
256+
else { assert.equal(this.events.length, 1); }
256257
const event = this.events.shift();
257258
assert.equal(event.eventName, eventName);
258259
return event.data;

src/services/codefixes/importFixes.ts

+27-11
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,23 @@ namespace ts.codefix {
138138

139139
const currentTokenMeaning = getMeaningFromLocation(token);
140140
if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) {
141-
const symbol = checker.getAliasedSymbol(checker.getSymbolAtLocation(token));
142-
return getCodeActionForImport(symbol, /*isDefault*/ false, /*isNamespaceImport*/ true);
141+
const umdSymbol = checker.getSymbolAtLocation(token);
142+
let symbol: ts.Symbol;
143+
let symbolName: string;
144+
if (umdSymbol.flags & ts.SymbolFlags.Alias) {
145+
symbol = checker.getAliasedSymbol(umdSymbol);
146+
symbolName = name;
147+
}
148+
else if (isJsxOpeningLikeElement(token.parent) && token.parent.tagName === token) {
149+
// The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`.
150+
symbol = checker.getAliasedSymbol(checker.resolveNameAtLocation(token, checker.getJsxNamespace(), SymbolFlags.Value));
151+
symbolName = symbol.name;
152+
}
153+
else {
154+
Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here");
155+
}
156+
157+
return getCodeActionForImport(symbol, symbolName, /*isDefault*/ false, /*isNamespaceImport*/ true);
143158
}
144159

145160
const candidateModules = checker.getAmbientModules();
@@ -159,15 +174,15 @@ namespace ts.codefix {
159174
if (localSymbol && localSymbol.name === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) {
160175
// check if this symbol is already used
161176
const symbolId = getUniqueSymbolId(localSymbol);
162-
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, /*isDefault*/ true));
177+
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, name, /*isDefault*/ true));
163178
}
164179
}
165180

166181
// check exports with the same name
167182
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExports(name, moduleSymbol);
168183
if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
169184
const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName);
170-
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol));
185+
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, name));
171186
}
172187
}
173188

@@ -218,7 +233,7 @@ namespace ts.codefix {
218233
return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false;
219234
}
220235

221-
function getCodeActionForImport(moduleSymbol: Symbol, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] {
236+
function getCodeActionForImport(moduleSymbol: Symbol, symbolName: string, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] {
222237
const existingDeclarations = getImportDeclarations(moduleSymbol);
223238
if (existingDeclarations.length > 0) {
224239
// With an existing import statement, there are more than one actions the user can do.
@@ -375,10 +390,10 @@ namespace ts.codefix {
375390
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport());
376391
const changeTracker = createChangeTracker();
377392
const importClause = isDefault
378-
? createImportClause(createIdentifier(name), /*namedBindings*/ undefined)
393+
? createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined)
379394
: isNamespaceImport
380-
? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(name)))
381-
: createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name))]));
395+
? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName)))
396+
: createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))]));
382397
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifierWithoutQuotes));
383398
if (!lastImportDeclaration) {
384399
changeTracker.insertNodeAt(sourceFile, sourceFile.getStart(), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` });
@@ -392,7 +407,7 @@ namespace ts.codefix {
392407
// are there are already a new line seperating code and import statements.
393408
return createCodeAction(
394409
Diagnostics.Import_0_from_1,
395-
[name, `"${moduleSpecifierWithoutQuotes}"`],
410+
[symbolName, `"${moduleSpecifierWithoutQuotes}"`],
396411
changeTracker.getChanges(),
397412
"NewImport",
398413
moduleSpecifierWithoutQuotes
@@ -412,8 +427,9 @@ namespace ts.codefix {
412427
removeFileExtension(getRelativePath(moduleFileName, sourceDirectory));
413428

414429
function tryGetModuleNameFromAmbientModule(): string {
415-
if (moduleSymbol.valueDeclaration.kind !== SyntaxKind.SourceFile) {
416-
return moduleSymbol.name;
430+
const decl = moduleSymbol.valueDeclaration;
431+
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
432+
return decl.name.text;
417433
}
418434
}
419435

src/services/findAllReferences.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ namespace ts.FindAllReferences {
185185
if (entry.type === "node") {
186186
const { node } = entry;
187187
return { textSpan: getTextSpan(node), fileName: node.getSourceFile().fileName, ...implementationKindDisplayParts(node, checker) };
188-
} else {
188+
}
189+
else {
189190
const { textSpan, fileName } = entry;
190191
return { textSpan, fileName, kind: ScriptElementKind.unknown, displayParts: [] };
191192
}

src/services/importTracker.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,8 @@ namespace ts.FindAllReferences {
530530
if (parent.kind === SyntaxKind.VariableDeclaration) {
531531
const p = parent as ts.VariableDeclaration;
532532
return p.parent.kind === ts.SyntaxKind.CatchClause ? undefined : p.parent.parent.kind === SyntaxKind.VariableStatement ? p.parent.parent : undefined;
533-
} else {
533+
}
534+
else {
534535
return parent;
535536
}
536537
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: react
4+
5+
// @Filename: /node_modules/@types/react/index.d.ts
6+
////export = React;
7+
////export as namespace React;
8+
////declare namespace React {
9+
//// export class Component { render(): JSX.Element | null; }
10+
////}
11+
////declare global {
12+
//// namespace JSX {
13+
//// interface Element {}
14+
//// }
15+
////}
16+
17+
// @Filename: /a.tsx
18+
////[|import { Component } from "react";
19+
////export class MyMap extends Component { }
20+
////<MyMap/>;|]
21+
22+
goTo.file("/a.tsx");
23+
24+
verify.importFixAtPosition([
25+
`import { Component } from "react";
26+
import * as React from "react";
27+
export class MyMap extends Component { }
28+
<MyMap/>;`]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: react
4+
5+
// @Filename: /node_modules/@types/react/index.d.ts
6+
////export = React;
7+
////export as namespace React;
8+
////declare namespace React {
9+
//// export class Component { render(): JSX.Element | null; }
10+
////}
11+
////declare global {
12+
//// namespace JSX {
13+
//// interface Element {}
14+
//// }
15+
////}
16+
17+
// @Filename: /a.tsx
18+
////[|import { Component } from "react";
19+
////export class MyMap extends Component { }
20+
////<MyMap></MyMap>;|]
21+
22+
goTo.file("/a.tsx");
23+
24+
verify.importFixAtPosition([
25+
`import { Component } from "react";
26+
import * as React from "react";
27+
export class MyMap extends Component { }
28+
<MyMap></MyMap>;`]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// https://github.com/Microsoft/TypeScript/issues/16065
4+
5+
// @jsx: react
6+
// @jsxFactory: factory
7+
8+
// @Filename: /factory.ts
9+
////export function factory() { return {}; }
10+
////declare global {
11+
//// namespace JSX {
12+
//// interface Element {}
13+
//// }
14+
////}
15+
16+
// @Filename: /a.tsx
17+
////[|<div/>|]
18+
19+
goTo.file("/a.tsx");
20+
verify.not
21+
verify.importFixAtPosition([]);

0 commit comments

Comments
 (0)