diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 6edba58cb1167..49b259ea695b9 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1,3 +1,4 @@ +/// /// /* @internal */ @@ -6,8 +7,8 @@ namespace ts { export const enum ModuleInstanceState { NonInstantiated = 0, - Instantiated = 1, - ConstEnumOnly = 2 + Instantiated = 1, + ConstEnumOnly = 2 } const enum Reachability { @@ -208,6 +209,9 @@ namespace ts { return "__export"; case SyntaxKind.ExportAssignment: return (node).isExportEquals ? "export=" : "default"; + case SyntaxKind.BinaryExpression: + // Binary expression case is for JS module 'module.exports = expr' + return "export="; case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: return node.flags & NodeFlags.Default ? "default" : undefined; @@ -1069,7 +1073,7 @@ namespace ts { return "__" + indexOf((node.parent).parameters, node); } - function bind(node: Node) { + function bind(node: Node): void { if (!node) { return; } @@ -1145,9 +1149,18 @@ namespace ts { function bindWorker(node: Node) { switch (node.kind) { + /* Strict mode checks */ case SyntaxKind.Identifier: return checkStrictModeIdentifier(node); case SyntaxKind.BinaryExpression: + if (isInJavaScriptFile(node)) { + if (isExportsPropertyAssignment(node)) { + bindExportsPropertyAssignment(node); + } + else if (isModuleExportsAssignment(node)) { + bindModuleExportsAssignment(node); + } + } return checkStrictModeBinaryExpression(node); case SyntaxKind.CatchClause: return checkStrictModeCatchClause(node); @@ -1213,6 +1226,14 @@ namespace ts { checkStrictModeFunctionName(node); const bindingName = (node).name ? (node).name.text : "__function"; return bindAnonymousDeclaration(node, SymbolFlags.Function, bindingName); + + case SyntaxKind.CallExpression: + if (isInJavaScriptFile(node)) { + bindCallExpression(node); + } + break; + + // Members of classes, interfaces, and modules case SyntaxKind.ClassExpression: case SyntaxKind.ClassDeclaration: return bindClassLikeDeclaration(node); @@ -1224,6 +1245,8 @@ namespace ts { return bindEnumDeclaration(node); case SyntaxKind.ModuleDeclaration: return bindModuleDeclaration(node); + + // Imports and exports case SyntaxKind.ImportEqualsDeclaration: case SyntaxKind.NamespaceImport: case SyntaxKind.ImportSpecifier: @@ -1243,16 +1266,21 @@ namespace ts { function bindSourceFileIfExternalModule() { setExportContextFlag(file); if (isExternalModule(file)) { - bindAnonymousDeclaration(file, SymbolFlags.ValueModule, `"${removeFileExtension(file.fileName)}"`); + bindSourceFileAsExternalModule(); } } - function bindExportAssignment(node: ExportAssignment) { + function bindSourceFileAsExternalModule() { + bindAnonymousDeclaration(file, SymbolFlags.ValueModule, `"${removeFileExtension(file.fileName) }"`); + } + + function bindExportAssignment(node: ExportAssignment | BinaryExpression) { + const boundExpression = node.kind === SyntaxKind.ExportAssignment ? (node).expression : (node).right; if (!container.symbol || !container.symbol.exports) { // Export assignment in some sort of block construct bindAnonymousDeclaration(node, SymbolFlags.Alias, getDeclarationName(node)); } - else if (node.expression.kind === SyntaxKind.Identifier) { + else if (boundExpression.kind === SyntaxKind.Identifier) { // An export default clause with an identifier exports all meanings of that identifier declareSymbol(container.symbol.exports, container.symbol, node, SymbolFlags.Alias, SymbolFlags.PropertyExcludes | SymbolFlags.AliasExcludes); } @@ -1279,6 +1307,34 @@ namespace ts { } } + function setCommonJsModuleIndicator(node: Node) { + if (!file.commonJsModuleIndicator) { + file.commonJsModuleIndicator = node; + bindSourceFileAsExternalModule(); + } + } + + function bindExportsPropertyAssignment(node: BinaryExpression) { + // When we create a property via 'exports.foo = bar', the 'exports.foo' property access + // expression is the declaration + setCommonJsModuleIndicator(node); + declareSymbol(file.symbol.exports, file.symbol, node.left, SymbolFlags.Property | SymbolFlags.Export, SymbolFlags.None); + } + + function bindModuleExportsAssignment(node: BinaryExpression) { + // 'module.exports = expr' assignment + setCommonJsModuleIndicator(node); + bindExportAssignment(node); + } + + function bindCallExpression(node: CallExpression) { + // We're only inspecting call expressions to detect CommonJS modules, so we can skip + // this check if we've already seen the module indicator + if (!file.commonJsModuleIndicator && isRequireCall(node)) { + setCommonJsModuleIndicator(node); + } + } + function bindClassLikeDeclaration(node: ClassLikeDeclaration) { if (node.kind === SyntaxKind.ClassDeclaration) { bindBlockScopedDeclaration(node, SymbolFlags.Class, SymbolFlags.ClassExcludes); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f1593abb17763..8c50eafded386 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -364,7 +364,7 @@ namespace ts { } function isGlobalSourceFile(node: Node) { - return node.kind === SyntaxKind.SourceFile && !isExternalModule(node); + return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(node); } function getSymbol(symbols: SymbolTable, name: string, meaning: SymbolFlags): Symbol { @@ -480,7 +480,7 @@ namespace ts { } switch (location.kind) { case SyntaxKind.SourceFile: - if (!isExternalModule(location)) break; + if (!isExternalOrCommonJsModule(location)) break; case SyntaxKind.ModuleDeclaration: const moduleExports = getSymbolOfNode(location).exports; if (location.kind === SyntaxKind.SourceFile || @@ -990,11 +990,16 @@ namespace ts { // Module names are escaped in our symbol table. However, string literal values aren't. // Escape the name in the "require(...)" clause to ensure we find the right symbol. - const moduleName = escapeIdentifier(moduleReferenceLiteral.text); + let moduleName = escapeIdentifier(moduleReferenceLiteral.text); if (moduleName === undefined) { return; } + + if (moduleName.indexOf("!") >= 0) { + moduleName = moduleName.substr(0, moduleName.indexOf("!")); + } + const isRelative = isExternalModuleNameRelative(moduleName); if (!isRelative) { const symbol = getSymbol(globals, "\"" + moduleName + "\"", SymbolFlags.ValueModule); @@ -1205,7 +1210,7 @@ namespace ts { } switch (location.kind) { case SyntaxKind.SourceFile: - if (!isExternalModule(location)) { + if (!isExternalOrCommonJsModule(location)) { break; } case SyntaxKind.ModuleDeclaration: @@ -1387,7 +1392,7 @@ namespace ts { function hasExternalModuleSymbol(declaration: Node) { return (declaration.kind === SyntaxKind.ModuleDeclaration && (declaration).name.kind === SyntaxKind.StringLiteral) || - (declaration.kind === SyntaxKind.SourceFile && isExternalModule(declaration)); + (declaration.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule(declaration)); } function hasVisibleDeclarations(symbol: Symbol): SymbolVisibilityResult { @@ -2061,7 +2066,7 @@ namespace ts { } } else if (node.kind === SyntaxKind.SourceFile) { - return isExternalModule(node) ? node : undefined; + return isExternalOrCommonJsModule(node) ? node : undefined; } } Debug.fail("getContainingModule cant reach here"); @@ -2182,7 +2187,7 @@ namespace ts { case SyntaxKind.SourceFile: return true; - // Export assignements do not create name bindings outside the module + // Export assignments do not create name bindings outside the module case SyntaxKind.ExportAssignment: return false; @@ -2567,6 +2572,14 @@ namespace ts { if (declaration.kind === SyntaxKind.ExportAssignment) { return links.type = checkExpression((declaration).expression); } + // Handle module.exports = expr + if (declaration.kind === SyntaxKind.BinaryExpression) { + return links.type = checkExpression((declaration).right); + } + // Handle exports.p = expr + if (declaration.kind === SyntaxKind.PropertyAccessExpression) { + return checkExpressionCached((declaration.parent).right); + } // Handle variable, parameter or property if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { return unknownType; @@ -3841,6 +3854,18 @@ namespace ts { return result; } + function resolveExternalModuleTypeByLiteral(name: StringLiteral) { + const moduleSym = resolveExternalModuleName(name, name); + if (moduleSym) { + const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); + if (resolvedModuleSymbol) { + return getTypeOfSymbol(resolvedModuleSymbol); + } + } + + return anyType; + } + function getReturnTypeOfSignature(signature: Signature): Type { if (!signature.resolvedReturnType) { if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) { @@ -9426,6 +9451,12 @@ namespace ts { return anyType; } } + + // In JavaScript files, calls to any identifier 'require' are treated as external module imports + if (isInJavaScriptFile(node) && isRequireCall(node)) { + return resolveExternalModuleTypeByLiteral(node.arguments[0]); + } + return getReturnTypeOfSignature(signature); } @@ -12067,7 +12098,7 @@ namespace ts { // In case of variable declaration, node.parent is variable statement so look at the variable statement's parent const parent = getDeclarationContainer(node); - if (parent.kind === SyntaxKind.SourceFile && isExternalModule(parent)) { + if (parent.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule(parent)) { // If the declaration happens to be in external module, report error that require and exports are reserved keywords error(name, Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module, declarationNameToString(name), declarationNameToString(name)); @@ -14150,7 +14181,7 @@ namespace ts { forEach(node.statements, checkSourceElement); checkFunctionAndClassExpressionBodies(node); - if (isExternalModule(node)) { + if (isExternalOrCommonJsModule(node)) { checkExternalModuleExports(node); } @@ -14253,7 +14284,7 @@ namespace ts { switch (location.kind) { case SyntaxKind.SourceFile: - if (!isExternalModule(location)) { + if (!isExternalOrCommonJsModule(location)) { break; } case SyntaxKind.ModuleDeclaration: @@ -14999,16 +15030,16 @@ namespace ts { // Initialize global symbol table forEach(host.getSourceFiles(), file => { - if (!isExternalModule(file)) { + if (!isExternalOrCommonJsModule(file)) { mergeSymbolTable(globals, file.locals); } }); - // Initialize special symbols getSymbolLinks(undefinedSymbol).type = undefinedType; getSymbolLinks(argumentsSymbol).type = getGlobalType("IArguments"); getSymbolLinks(unknownSymbol).type = unknownType; globals[undefinedSymbol.name] = undefinedSymbol; + // Initialize special types globalArrayType = getGlobalType("Array", /*arity*/ 1); globalObjectType = getGlobalType("Object"); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2ce3bbfaf3bc2..c866cf41a928a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -739,12 +739,7 @@ namespace ts { * List of supported extensions in order of file resolution precedence. */ export const supportedExtensions = [".ts", ".tsx", ".d.ts"]; - /** - * List of extensions that will be used to look for external modules. - * This list is kept separate from supportedExtensions to for cases when we'll allow to include .js files in compilation, - * but still would like to load only TypeScript files as modules - */ - export const moduleFileExtensions = supportedExtensions; + export const supportedJsExtensions = supportedExtensions.concat(".js", ".jsx"); export function isSupportedSourceFileName(fileName: string) { if (!fileName) { return false; } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 18e9cdea4062f..e52a9be73f707 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1,5 +1,5 @@ -/// /// +/// namespace ts { /* @internal */ export let parseTime = 0; @@ -534,7 +534,8 @@ namespace ts { let parseErrorBeforeNextFinishedNode = false; export function parseSourceFile(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): SourceFile { - initializeState(fileName, _sourceText, languageVersion, _syntaxCursor); + const isJavaScriptFile = hasJavaScriptFileExtension(fileName) || _sourceText.lastIndexOf("// @language=javascript", 0) === 0; + initializeState(fileName, _sourceText, languageVersion, isJavaScriptFile, _syntaxCursor); const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes); @@ -543,7 +544,7 @@ namespace ts { return result; } - function initializeState(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor) { + function initializeState(fileName: string, _sourceText: string, languageVersion: ScriptTarget, isJavaScriptFile: boolean, _syntaxCursor: IncrementalParser.SyntaxCursor) { NodeConstructor = objectAllocator.getNodeConstructor(); SourceFileConstructor = objectAllocator.getSourceFileConstructor(); @@ -556,14 +557,14 @@ namespace ts { identifierCount = 0; nodeCount = 0; - contextFlags = isJavaScript(fileName) ? ParserContextFlags.JavaScriptFile : ParserContextFlags.None; + contextFlags = isJavaScriptFile ? ParserContextFlags.JavaScriptFile : ParserContextFlags.None; parseErrorBeforeNextFinishedNode = false; // Initialize and prime the scanner before parsing the source elements. scanner.setText(sourceText); scanner.setOnError(scanError); scanner.setScriptTarget(languageVersion); - scanner.setLanguageVariant(isTsx(fileName) ? LanguageVariant.JSX : LanguageVariant.Standard); + scanner.setLanguageVariant(allowsJsxExpressions(fileName) ? LanguageVariant.JSX : LanguageVariant.Standard); } function clearState() { @@ -582,6 +583,10 @@ namespace ts { function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean): SourceFile { sourceFile = createSourceFile(fileName, languageVersion); + if (contextFlags & ParserContextFlags.JavaScriptFile) { + sourceFile.parserContextFlags = ParserContextFlags.JavaScriptFile; + } + // Prime the scanner. token = nextToken(); processReferenceComments(sourceFile); @@ -604,7 +609,7 @@ namespace ts { // If this is a javascript file, proactively see if we can get JSDoc comments for // relevant nodes in the file. We'll use these to provide typing informaion if they're // available. - if (isJavaScript(fileName)) { + if (isSourceFileJavaScript(sourceFile)) { addJSDocComments(); } @@ -677,7 +682,7 @@ namespace ts { sourceFile.languageVersion = languageVersion; sourceFile.fileName = normalizePath(fileName); sourceFile.flags = fileExtensionIs(sourceFile.fileName, ".d.ts") ? NodeFlags.DeclarationFile : 0; - sourceFile.languageVariant = isTsx(sourceFile.fileName) ? LanguageVariant.JSX : LanguageVariant.Standard; + sourceFile.languageVariant = allowsJsxExpressions(sourceFile.fileName) ? LanguageVariant.JSX : LanguageVariant.Standard; return sourceFile; } @@ -5519,7 +5524,7 @@ namespace ts { } export function parseJSDocTypeExpressionForTests(content: string, start: number, length: number) { - initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined); + initializeState("file.js", content, ScriptTarget.Latest, /*isJavaScriptFile*/ true, /*_syntaxCursor:*/ undefined); const jsDocTypeExpression = parseJSDocTypeExpression(start, length); const diagnostics = parseDiagnostics; clearState(); @@ -5840,7 +5845,7 @@ namespace ts { } export function parseIsolatedJSDocComment(content: string, start: number, length: number) { - initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined); + initializeState("file.js", content, ScriptTarget.Latest, /*isJavaScriptFile*/ true, /*_syntaxCursor:*/ undefined); const jsDocComment = parseJSDocComment(/*parent:*/ undefined, start, length); const diagnostics = parseDiagnostics; clearState(); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8f031a4de4342..08321da82e9cf 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -53,13 +53,13 @@ namespace ts { if (getRootLength(moduleName) !== 0 || nameStartsWithDotSlashOrDotDotSlash(moduleName)) { const failedLookupLocations: string[] = []; const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); - let resolvedFileName = loadNodeModuleFromFile(candidate, failedLookupLocations, host); + let resolvedFileName = loadNodeModuleFromFile(supportedJsExtensions, candidate, failedLookupLocations, host); if (resolvedFileName) { return { resolvedModule: { resolvedFileName }, failedLookupLocations }; } - resolvedFileName = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host); + resolvedFileName = loadNodeModuleFromDirectory(supportedJsExtensions, candidate, failedLookupLocations, host); return resolvedFileName ? { resolvedModule: { resolvedFileName }, failedLookupLocations } : { resolvedModule: undefined, failedLookupLocations }; @@ -69,8 +69,8 @@ namespace ts { } } - function loadNodeModuleFromFile(candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string { - return forEach(moduleFileExtensions, tryLoad); + function loadNodeModuleFromFile(extensions: string[], candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string { + return forEach(extensions, tryLoad); function tryLoad(ext: string): string { const fileName = fileExtensionIs(candidate, ext) ? candidate : candidate + ext; @@ -84,7 +84,7 @@ namespace ts { } } - function loadNodeModuleFromDirectory(candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string { + function loadNodeModuleFromDirectory(extensions: string[], candidate: string, failedLookupLocation: string[], host: ModuleResolutionHost): string { const packageJsonPath = combinePaths(candidate, "package.json"); if (host.fileExists(packageJsonPath)) { @@ -100,7 +100,7 @@ namespace ts { } if (jsonContent.typings) { - const result = loadNodeModuleFromFile(normalizePath(combinePaths(candidate, jsonContent.typings)), failedLookupLocation, host); + const result = loadNodeModuleFromFile(extensions, normalizePath(combinePaths(candidate, jsonContent.typings)), failedLookupLocation, host); if (result) { return result; } @@ -111,7 +111,7 @@ namespace ts { failedLookupLocation.push(packageJsonPath); } - return loadNodeModuleFromFile(combinePaths(candidate, "index"), failedLookupLocation, host); + return loadNodeModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocation, host); } function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { @@ -122,12 +122,12 @@ namespace ts { if (baseName !== "node_modules") { const nodeModulesFolder = combinePaths(directory, "node_modules"); const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); - let result = loadNodeModuleFromFile(candidate, failedLookupLocations, host); + let result = loadNodeModuleFromFile(supportedExtensions, candidate, failedLookupLocations, host); if (result) { return { resolvedModule: { resolvedFileName: result, isExternalLibraryImport: true }, failedLookupLocations }; } - result = loadNodeModuleFromDirectory(candidate, failedLookupLocations, host); + result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, host); if (result) { return { resolvedModule: { resolvedFileName: result, isExternalLibraryImport: true }, failedLookupLocations }; } @@ -162,9 +162,10 @@ namespace ts { const failedLookupLocations: string[] = []; let referencedSourceFile: string; + const extensions = compilerOptions.allowNonTsExtensions ? supportedJsExtensions : supportedExtensions; while (true) { searchName = normalizePath(combinePaths(searchPath, moduleName)); - referencedSourceFile = forEach(supportedExtensions, extension => { + referencedSourceFile = forEach(extensions, extension => { if (extension === ".tsx" && !compilerOptions.jsx) { // resolve .tsx files only if jsx support is enabled // 'logical not' handles both undefined and None cases @@ -688,45 +689,60 @@ namespace ts { return; } + const isJavaScriptFile = isSourceFileJavaScript(file); + let imports: LiteralExpression[]; for (const node of file.statements) { - collect(node, /* allowRelativeModuleNames */ true); + collect(node, /* allowRelativeModuleNames */ true, /* collectOnlyRequireCalls */ false); } file.imports = imports || emptyArray; - function collect(node: Node, allowRelativeModuleNames: boolean): void { - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ExportDeclaration: - let moduleNameExpr = getExternalModuleName(node); - if (!moduleNameExpr || moduleNameExpr.kind !== SyntaxKind.StringLiteral) { + return; + + function collect(node: Node, allowRelativeModuleNames: boolean, collectOnlyRequireCalls: boolean): void { + if (!collectOnlyRequireCalls) { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportDeclaration: + let moduleNameExpr = getExternalModuleName(node); + if (!moduleNameExpr || moduleNameExpr.kind !== SyntaxKind.StringLiteral) { + break; + } + if (!(moduleNameExpr).text) { + break; + } + + if (allowRelativeModuleNames || !isExternalModuleNameRelative((moduleNameExpr).text)) { + (imports || (imports = [])).push(moduleNameExpr); + } break; - } - if (!(moduleNameExpr).text) { + case SyntaxKind.ModuleDeclaration: + if ((node).name.kind === SyntaxKind.StringLiteral && (node.flags & NodeFlags.Ambient || isDeclarationFile(file))) { + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An AmbientExternalModuleDeclaration declares an external module. + // This type of declaration is permitted only in the global module. + // The StringLiteral must specify a top - level external module name. + // Relative external module names are not permitted + forEachChild((node).body, node => { + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An ExternalImportDeclaration in anAmbientExternalModuleDeclaration may reference other external modules + // only through top - level external module names. Relative external module names are not permitted. + collect(node, /* allowRelativeModuleNames */ false, collectOnlyRequireCalls); + }); + } break; - } + } + } - if (allowRelativeModuleNames || !isExternalModuleNameRelative((moduleNameExpr).text)) { - (imports || (imports = [])).push(moduleNameExpr); - } - break; - case SyntaxKind.ModuleDeclaration: - if ((node).name.kind === SyntaxKind.StringLiteral && (node.flags & NodeFlags.Ambient || isDeclarationFile(file))) { - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An AmbientExternalModuleDeclaration declares an external module. - // This type of declaration is permitted only in the global module. - // The StringLiteral must specify a top - level external module name. - // Relative external module names are not permitted - forEachChild((node).body, node => { - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An ExternalImportDeclaration in anAmbientExternalModuleDeclaration may reference other external modules - // only through top - level external module names. Relative external module names are not permitted. - collect(node, /* allowRelativeModuleNames */ false); - }); - } - break; + if (isJavaScriptFile) { + if (isRequireCall(node)) { + (imports || (imports = [])).push((node).arguments[0]); + } + else { + forEachChild(node, node => collect(node, allowRelativeModuleNames, /* collectOnlyRequireCalls */ true)); + } } } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 08313f832a2ad..22b03da8a8dc9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -773,7 +773,8 @@ namespace ts { expression?: Expression; } - export interface BinaryExpression extends Expression { + // Binary expressions can be declarations if they are 'exports.foo = bar' expressions in JS files + export interface BinaryExpression extends Expression, Declaration { left: Expression; operatorToken: Node; right: Expression; @@ -834,7 +835,7 @@ namespace ts { properties: NodeArray; } - export interface PropertyAccessExpression extends MemberExpression { + export interface PropertyAccessExpression extends MemberExpression, Declaration { expression: LeftHandSideExpression; dotToken: Node; name: Identifier; @@ -1284,6 +1285,8 @@ namespace ts { // The first node that causes this file to be an external module /* @internal */ externalModuleIndicator: Node; + // The first node that causes this file to be a CommonJS module + /* @internal */ commonJsModuleIndicator: Node; /* @internal */ identifiers: Map; /* @internal */ nodeCount: number; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 17d780df57458..7fdab45c2ed48 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1,4 +1,3 @@ -/// /// /* @internal */ @@ -362,6 +361,10 @@ namespace ts { return file.externalModuleIndicator !== undefined; } + export function isExternalOrCommonJsModule(file: SourceFile): boolean { + return (file.externalModuleIndicator || file.commonJsModuleIndicator) !== undefined; + } + export function isDeclarationFile(file: SourceFile): boolean { return (file.flags & NodeFlags.DeclarationFile) !== 0; } @@ -1056,6 +1059,57 @@ namespace ts { return node.kind === SyntaxKind.ImportEqualsDeclaration && (node).moduleReference.kind !== SyntaxKind.ExternalModuleReference; } + export function isSourceFileJavaScript(file: SourceFile): boolean { + return isInJavaScriptFile(file); + } + + export function isInJavaScriptFile(node: Node): boolean { + return node && !!(node.parserContextFlags & ParserContextFlags.JavaScriptFile); + } + + /** + * Returns true if the node is a CallExpression to the identifier 'require' with + * exactly one string literal argument. + * This function does not test if the node is in a JavaScript file or not. + */ + export function isRequireCall(expression: Node): expression is CallExpression { + // of the form 'require("name")' + return expression.kind === SyntaxKind.CallExpression && + (expression).expression.kind === SyntaxKind.Identifier && + ((expression).expression).text === "require" && + (expression).arguments.length === 1 && + (expression).arguments[0].kind === SyntaxKind.StringLiteral; + } + + /** + * Returns true if the node is an assignment to a property on the identifier 'exports'. + * This function does not test if the node is in a JavaScript file or not. + */ + export function isExportsPropertyAssignment(expression: Node): boolean { + // of the form 'exports.name = expr' where 'name' and 'expr' are arbitrary + return isInJavaScriptFile(expression) && + (expression.kind === SyntaxKind.BinaryExpression) && + ((expression).operatorToken.kind === SyntaxKind.EqualsToken) && + ((expression).left.kind === SyntaxKind.PropertyAccessExpression) && + (((expression).left).expression.kind === SyntaxKind.Identifier) && + (((((expression).left).expression)).text === "exports"); + } + + /** + * Returns true if the node is an assignment to the property access expression 'module.exports'. + * This function does not test if the node is in a JavaScript file or not. + */ + export function isModuleExportsAssignment(expression: Node): boolean { + // of the form 'module.exports = expr' where 'expr' is arbitrary + return isInJavaScriptFile(expression) && + (expression.kind === SyntaxKind.BinaryExpression) && + ((expression).operatorToken.kind === SyntaxKind.EqualsToken) && + ((expression).left.kind === SyntaxKind.PropertyAccessExpression) && + (((expression).left).expression.kind === SyntaxKind.Identifier) && + (((((expression).left).expression)).text === "module") && + (((expression).left).name.text === "exports"); + } + export function getExternalModuleName(node: Node): Expression { if (node.kind === SyntaxKind.ImportDeclaration) { return (node).moduleSpecifier; @@ -2210,12 +2264,12 @@ namespace ts { return symbol && symbol.valueDeclaration && (symbol.valueDeclaration.flags & NodeFlags.Default) ? symbol.valueDeclaration.localSymbol : undefined; } - export function isJavaScript(fileName: string) { - return fileExtensionIs(fileName, ".js"); + export function hasJavaScriptFileExtension(fileName: string) { + return fileExtensionIs(fileName, ".js") || fileExtensionIs(fileName, ".jsx"); } - export function isTsx(fileName: string) { - return fileExtensionIs(fileName, ".tsx"); + export function allowsJsxExpressions(fileName: string) { + return fileExtensionIs(fileName, ".tsx") || fileExtensionIs(fileName, ".jsx"); } /** diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index bf8b9493829cf..1c8593a9e1294 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -223,10 +223,22 @@ namespace FourSlash { // Add input file which has matched file name with the given reference-file path. // This is necessary when resolveReference flag is specified - private addMatchedInputFile(referenceFilePath: string) { - const inputFile = this.inputFiles[referenceFilePath]; - if (inputFile && !Harness.isLibraryFile(referenceFilePath)) { - this.languageServiceAdapterHost.addScript(referenceFilePath, inputFile); + private addMatchedInputFile(referenceFilePath: string, extensions: string[]) { + const inputFiles = this.inputFiles; + const languageServiceAdapterHost = this.languageServiceAdapterHost; + if (!extensions) { + tryAdd(referenceFilePath); + } + else { + tryAdd(referenceFilePath) || ts.forEach(extensions, ext => tryAdd(referenceFilePath + ext)); + } + + function tryAdd(path: string) { + const inputFile = inputFiles[path]; + if (inputFile && !Harness.isLibraryFile(path)) { + languageServiceAdapterHost.addScript(path, inputFile); + return true; + } } } @@ -280,15 +292,15 @@ namespace FourSlash { ts.forEach(referencedFiles, referenceFile => { // Fourslash insert tests/cases/fourslash into inputFile.unitName so we will properly append the same base directory to refFile path const referenceFilePath = this.basePath + "/" + referenceFile.fileName; - this.addMatchedInputFile(referenceFilePath); + this.addMatchedInputFile(referenceFilePath, /* extensions */ undefined); }); // Add import files into language-service host ts.forEach(importedFiles, importedFile => { // Fourslash insert tests/cases/fourslash into inputFile.unitName and import statement doesn't require ".ts" // so convert them before making appropriate comparison - const importedFilePath = this.basePath + "/" + importedFile.fileName + ".ts"; - this.addMatchedInputFile(importedFilePath); + const importedFilePath = this.basePath + "/" + importedFile.fileName; + this.addMatchedInputFile(importedFilePath, compilationOptions.allowNonTsExtensions ? ts.supportedJsExtensions : ts.supportedExtensions); }); // Check if no-default-lib flag is false and if so add default library @@ -2257,15 +2269,15 @@ namespace FourSlash { const details = this.getCompletionEntryDetails(item.name); if (documentation !== undefined) { - assert.equal(ts.displayPartsToString(details.documentation), documentation, assertionMessage("completion item documentation")); + assert.equal(ts.displayPartsToString(details.documentation), documentation, assertionMessage("completion item documentation for " + name)); } if (text !== undefined) { - assert.equal(ts.displayPartsToString(details.displayParts), text, assertionMessage("completion item detail text")); + assert.equal(ts.displayPartsToString(details.displayParts), text, assertionMessage("completion item detail text for " + name)); } } if (kind !== undefined) { - assert.equal(item.kind, kind, assertionMessage("completion item kind")); + assert.equal(item.kind, kind, assertionMessage("completion item kind for " + name)); } return; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 50dbef5a41000..faf0ad28eb526 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -197,7 +197,7 @@ namespace Harness.LanguageService { getHost() { return this.host; } getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } getClassifier(): ts.Classifier { return ts.createClassifier(); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJavaScriptFileExtension(fileName)); } } /// Shim adapter diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 6543dc67af0a0..333cea2745d6f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -371,6 +371,10 @@ namespace ts.server { openRefCount = 0; constructor(public projectService: ProjectService, public projectOptions?: ProjectOptions) { + if (projectOptions && projectOptions.files) { + // If files are listed explicitly, allow all extensions + projectOptions.compilerOptions.allowNonTsExtensions = true; + } this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions); } @@ -461,6 +465,7 @@ namespace ts.server { setProjectOptions(projectOptions: ProjectOptions) { this.projectOptions = projectOptions; if (projectOptions.compilerOptions) { + projectOptions.compilerOptions.allowNonTsExtensions = true; this.compilerService.setCompilerOptions(projectOptions.compilerOptions); } } @@ -1316,7 +1321,9 @@ namespace ts.server { this.setCompilerOptions(opt); } else { - this.setCompilerOptions(ts.getDefaultCompilerOptions()); + const defaultOpts = ts.getDefaultCompilerOptions(); + defaultOpts.allowNonTsExtensions = true; + this.setCompilerOptions(defaultOpts); } this.languageService = ts.createLanguageService(this.host, this.documentRegistry); this.classifier = ts.createClassifier(); diff --git a/src/services/services.ts b/src/services/services.ts index 113a52459e9cb..3d7a73c913955 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -801,6 +801,7 @@ namespace ts { public isDefaultLib: boolean; public hasNoDefaultLib: boolean; public externalModuleIndicator: Node; // The first node that causes this file to be an external module + public commonJsModuleIndicator: Node; // The first node that causes this file to be a CommonJS module public nodeCount: number; public identifierCount: number; public symbolCount: number; @@ -2124,7 +2125,7 @@ namespace ts { }; } - export function preProcessFile(sourceText: string, readImportFiles = true): PreProcessedFileInfo { + export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): PreProcessedFileInfo { let referencedFiles: FileReference[] = []; let importedFiles: FileReference[] = []; let ambientExternalModules: string[]; @@ -2162,119 +2163,69 @@ namespace ts { }); } - function processImport(): void { - scanner.setText(sourceText); - let token = scanner.scan(); - // Look for: - // import "mod"; - // import d from "mod" - // import {a as A } from "mod"; - // import * as NS from "mod" - // import d, {a, b as B} from "mod" - // import i = require("mod"); - // - // export * from "mod" - // export {a as b} from "mod" - // export import i = require("mod") - - while (token !== SyntaxKind.EndOfFileToken) { - if (token === SyntaxKind.DeclareKeyword) { - // declare module "mod" - token = scanner.scan(); - if (token === SyntaxKind.ModuleKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.StringLiteral) { - recordAmbientExternalModule(); - continue; - } - } - } - else if (token === SyntaxKind.ImportKeyword) { + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeDeclare(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.DeclareKeyword) { + // declare module "mod" + token = scanner.scan(); + if (token === SyntaxKind.ModuleKeyword) { token = scanner.scan(); if (token === SyntaxKind.StringLiteral) { - // import "mod"; - recordModuleName(); - continue; + recordAmbientExternalModule(); } - else { - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = scanner.scan(); - if (token === SyntaxKind.FromKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.StringLiteral) { - // import d from "mod"; - recordModuleName(); - continue - } - } - else if (token === SyntaxKind.EqualsToken) { - token = scanner.scan(); - if (token === SyntaxKind.RequireKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.OpenParenToken) { - token = scanner.scan(); - if (token === SyntaxKind.StringLiteral) { - // import i = require("mod"); - recordModuleName(); - continue; - } - } - } - } - else if (token === SyntaxKind.CommaToken) { - // consume comma and keep going - token = scanner.scan(); - } - else { - // unknown syntax - continue; - } - } + } + return true; + } - if (token === SyntaxKind.OpenBraceToken) { + return false; + } + + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeImport(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.ImportKeyword) { + token = scanner.scan(); + if (token === SyntaxKind.StringLiteral) { + // import "mod"; + recordModuleName(); + return true; + } + else { + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = scanner.scan(); + if (token === SyntaxKind.FromKeyword) { token = scanner.scan(); - // consume "{ a as B, c, d as D}" clauses - while (token !== SyntaxKind.CloseBraceToken) { - token = scanner.scan(); + if (token === SyntaxKind.StringLiteral) { + // import d from "mod"; + recordModuleName(); + return true; } - - if (token === SyntaxKind.CloseBraceToken) { - token = scanner.scan(); - if (token === SyntaxKind.FromKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.StringLiteral) { - // import {a as A} from "mod"; - // import d, {a, b as B} from "mod" - recordModuleName(); - } - } + } + else if (token === SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/* skipCurrentToken */ true)) { + return true; } } - else if (token === SyntaxKind.AsteriskToken) { + else if (token === SyntaxKind.CommaToken) { + // consume comma and keep going token = scanner.scan(); - if (token === SyntaxKind.AsKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = scanner.scan(); - if (token === SyntaxKind.FromKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.StringLiteral) { - // import * as NS from "mod" - // import d, * as NS from "mod" - recordModuleName(); - } - } - } - } + } + else { + // unknown syntax + return true; } } - } - else if (token === SyntaxKind.ExportKeyword) { - token = scanner.scan(); + if (token === SyntaxKind.OpenBraceToken) { token = scanner.scan(); // consume "{ a as B, c, d as D}" clauses - while (token !== SyntaxKind.CloseBraceToken) { + // make sure that it stops on EOF + while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { token = scanner.scan(); } @@ -2283,50 +2234,189 @@ namespace ts { if (token === SyntaxKind.FromKeyword) { token = scanner.scan(); if (token === SyntaxKind.StringLiteral) { - // export {a as A} from "mod"; - // export {a, b as B} from "mod" + // import {a as A} from "mod"; + // import d, {a, b as B} from "mod" recordModuleName(); } } } } else if (token === SyntaxKind.AsteriskToken) { + token = scanner.scan(); + if (token === SyntaxKind.AsKeyword) { + token = scanner.scan(); + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = scanner.scan(); + if (token === SyntaxKind.FromKeyword) { + token = scanner.scan(); + if (token === SyntaxKind.StringLiteral) { + // import * as NS from "mod" + // import d, * as NS from "mod" + recordModuleName(); + } + } + } + } + } + } + + return true; + } + + return false; + } + + function tryConsumeExport(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.ExportKeyword) { + token = scanner.scan(); + if (token === SyntaxKind.OpenBraceToken) { + token = scanner.scan(); + // consume "{ a as B, c, d as D}" clauses + // make sure it stops on EOF + while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { + token = scanner.scan(); + } + + if (token === SyntaxKind.CloseBraceToken) { token = scanner.scan(); if (token === SyntaxKind.FromKeyword) { token = scanner.scan(); if (token === SyntaxKind.StringLiteral) { - // export * from "mod" + // export {a as A} from "mod"; + // export {a, b as B} from "mod" recordModuleName(); } } } - else if (token === SyntaxKind.ImportKeyword) { + } + else if (token === SyntaxKind.AsteriskToken) { + token = scanner.scan(); + if (token === SyntaxKind.FromKeyword) { token = scanner.scan(); - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = scanner.scan(); - if (token === SyntaxKind.EqualsToken) { - token = scanner.scan(); - if (token === SyntaxKind.RequireKeyword) { - token = scanner.scan(); - if (token === SyntaxKind.OpenParenToken) { - token = scanner.scan(); - if (token === SyntaxKind.StringLiteral) { - // export import i = require("mod"); - recordModuleName(); - } - } - } + if (token === SyntaxKind.StringLiteral) { + // export * from "mod" + recordModuleName(); + } + } + } + else if (token === SyntaxKind.ImportKeyword) { + token = scanner.scan(); + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = scanner.scan(); + if (token === SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/* skipCurrentToken */ true)) { + return true; } } } } + + return true; + } + + return false; + } + + function tryConsumeRequireCall(skipCurrentToken: boolean): boolean { + let token = skipCurrentToken ? scanner.scan() : scanner.getToken(); + if (token === SyntaxKind.RequireKeyword) { + token = scanner.scan(); + if (token === SyntaxKind.OpenParenToken) { + token = scanner.scan(); + if (token === SyntaxKind.StringLiteral) { + // require("mod"); + recordModuleName(); + } + } + return true; + } + return false; + } + + function tryConsumeDefine(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.Identifier && scanner.getTokenValue() === "define") { + token = scanner.scan(); + if (token !== SyntaxKind.OpenParenToken) { + return true; + } + + token = scanner.scan(); + if (token === SyntaxKind.StringLiteral) { + // looks like define ("modname", ... - skip string literal and comma + token = scanner.scan(); + if (token === SyntaxKind.CommaToken) { + token = scanner.scan(); + } + else { + // unexpected token + return true; + } + } + + // should be start of dependency list + if (token !== SyntaxKind.OpenBracketToken) { + return true; + } + + // skip open bracket token = scanner.scan(); + let i = 0; + // scan until ']' or EOF + while (token !== SyntaxKind.CloseBracketToken && token !== SyntaxKind.EndOfFileToken) { + // record string literals as module names + if (token === SyntaxKind.StringLiteral) { + recordModuleName(); + i++; + } + + token = scanner.scan(); + } + return true; + } + return false; + } + + function processImports(): void { + scanner.setText(sourceText); + scanner.scan(); + // Look for: + // import "mod"; + // import d from "mod" + // import {a as A } from "mod"; + // import * as NS from "mod" + // import d, {a, b as B} from "mod" + // import i = require("mod"); + // + // export * from "mod" + // export {a as b} from "mod" + // export import i = require("mod") + // (for JavaScript files) require("mod") + + while (true) { + if (scanner.getToken() === SyntaxKind.EndOfFileToken) { + break; + } + + // check if at least one of alternative have moved scanner forward + if (tryConsumeDeclare() || + tryConsumeImport() || + tryConsumeExport() || + (detectJavaScriptImports && (tryConsumeRequireCall(/* skipCurrentToken */ false) || tryConsumeDefine()))) { + continue; + } + else { + scanner.scan(); + } + } + scanner.setText(undefined); } if (readImportFiles) { - processImport(); + processImports(); } processTripleSlashDirectives(); return { referencedFiles, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules }; @@ -2815,7 +2905,7 @@ namespace ts { // For JavaScript files, we don't want to report the normal typescript semantic errors. // Instead, we just report errors for using TypeScript-only constructs from within a // JavaScript file. - if (isJavaScript(fileName)) { + if (isSourceFileJavaScript(targetSourceFile)) { return getJavaScriptSemanticDiagnostics(targetSourceFile); } @@ -3054,7 +3144,7 @@ namespace ts { let typeChecker = program.getTypeChecker(); let syntacticStart = new Date().getTime(); let sourceFile = getValidSourceFile(fileName); - let isJavaScriptFile = isJavaScript(fileName); + let isJavaScriptFile = isSourceFileJavaScript(sourceFile); let isJsDocTagName = false; @@ -3875,22 +3965,25 @@ namespace ts { let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot, isJsDocTagName } = completionData; - let entries: CompletionEntry[]; if (isJsDocTagName) { // If the current position is a jsDoc tag name, only tag names should be provided for completion return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() }; } - if (isRightOfDot && isJavaScript(fileName)) { - entries = getCompletionEntriesFromSymbols(symbols); - addRange(entries, getJavaScriptCompletionEntries()); + let sourceFile = getValidSourceFile(fileName); + + let entries: CompletionEntry[] = []; + + if (isRightOfDot && isSourceFileJavaScript(sourceFile)) { + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries); + addRange(entries, getJavaScriptCompletionEntries(sourceFile, uniqueNames)); } else { if (!symbols || symbols.length === 0) { return undefined; } - entries = getCompletionEntriesFromSymbols(symbols); + getCompletionEntriesFromSymbols(symbols, entries); } // Add keywords if this is not a member completion list @@ -3900,26 +3993,23 @@ namespace ts { return { isMemberCompletion, isNewIdentifierLocation, entries }; - function getJavaScriptCompletionEntries(): CompletionEntry[] { + function getJavaScriptCompletionEntries(sourceFile: SourceFile, uniqueNames: Map): CompletionEntry[] { let entries: CompletionEntry[] = []; - let allNames: Map = {}; let target = program.getCompilerOptions().target; - for (let sourceFile of program.getSourceFiles()) { - let nameTable = getNameTable(sourceFile); - for (let name in nameTable) { - if (!allNames[name]) { - allNames[name] = name; - let displayName = getCompletionEntryDisplayName(name, target, /*performCharacterChecks:*/ true); - if (displayName) { - let entry = { - name: displayName, - kind: ScriptElementKind.warning, - kindModifiers: "", - sortText: "1" - }; - entries.push(entry); - } + let nameTable = getNameTable(sourceFile); + for (let name in nameTable) { + if (!uniqueNames[name]) { + uniqueNames[name] = name; + let displayName = getCompletionEntryDisplayName(name, target, /*performCharacterChecks:*/ true); + if (displayName) { + let entry = { + name: displayName, + kind: ScriptElementKind.warning, + kindModifiers: "", + sortText: "1" + }; + entries.push(entry); } } } @@ -3963,26 +4053,24 @@ namespace ts { }; } - function getCompletionEntriesFromSymbols(symbols: Symbol[]): CompletionEntry[] { + function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[]): Map { let start = new Date().getTime(); - let entries: CompletionEntry[] = []; - + let uniqueNames: Map = {}; if (symbols) { - let nameToSymbol: Map = {}; for (let symbol of symbols) { let entry = createCompletionEntry(symbol, location); if (entry) { let id = escapeIdentifier(entry.name); - if (!lookUp(nameToSymbol, id)) { + if (!lookUp(uniqueNames, id)) { entries.push(entry); - nameToSymbol[id] = symbol; + uniqueNames[id] = id; } } } } log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (new Date().getTime() - start)); - return entries; + return uniqueNames; } } diff --git a/src/services/shims.ts b/src/services/shims.ts index fca72e49d36e1..6b656ea2738fb 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -956,7 +956,8 @@ namespace ts { return this.forwardJSONCall( "getPreProcessedFileInfo('" + fileName + "')", () => { - var result = preProcessFile(sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength())); + // for now treat files as JavaScript + var result = preProcessFile(sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()), /* readImportFiles */ true, /* detectJavaScriptImports */ true); var convertResult = { referencedFiles: [], importedFiles: [], diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index df321647e5c52..02e36e185a56a 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -204,7 +204,7 @@ namespace ts.SignatureHelp { if (!candidates.length) { // We didn't have any sig help items produced by the TS compiler. If this is a JS // file, then see if we can figure out anything better. - if (isJavaScript(sourceFile.fileName)) { + if (isSourceFileJavaScript(sourceFile)) { return createJavaScriptSignatureHelpItems(argumentInfo); } diff --git a/tests/cases/fourslash/getJavaScriptSyntacticDiagnostics1.ts b/tests/cases/fourslash/getJavaScriptSyntacticDiagnostics1.ts deleted file mode 100644 index e409029276b62..0000000000000 --- a/tests/cases/fourslash/getJavaScriptSyntacticDiagnostics1.ts +++ /dev/null @@ -1,19 +0,0 @@ -/// - -// @allowNonTsExtensions: true -// @Filename: a.js -//// /** -//// * @type {number} -//// * @type {string} -//// */ -//// var v; - -verify.getSyntacticDiagnostics(`[ - { - "message": "\'type\' tag already specified.", - "start": 26, - "length": 4, - "category": "error", - "code": 1223 - } -]`); \ No newline at end of file diff --git a/tests/cases/fourslash/javaScriptModules12.ts b/tests/cases/fourslash/javaScriptModules12.ts new file mode 100644 index 0000000000000..c391f150963f1 --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules12.ts @@ -0,0 +1,57 @@ +/// + +// Invocations of 'require' stop top-level variables from becoming global + +// @allowNonTsExtensions: true + +// @Filename: mod1.js +//// var x = require('fs'); +//// /*1*/ + +// @Filename: mod2.js +//// var y; +//// if(true) { +//// y = require('fs'); +//// } +//// /*2*/ + +// @Filename: glob1.js +//// var a = require; +//// /*3*/ + +// @Filename: glob2.js +//// var b = ''; +//// /*4*/ + +// @Filename: consumer.js +//// /*5*/ + +goTo.marker('1'); +verify.completionListContains('x'); +verify.not.completionListContains('y'); +verify.completionListContains('a'); +verify.completionListContains('b'); + +goTo.marker('2'); +verify.not.completionListContains('x'); +verify.completionListContains('y'); +verify.completionListContains('a'); +verify.completionListContains('b'); + +goTo.marker('3'); +verify.not.completionListContains('x'); +verify.not.completionListContains('y'); +verify.completionListContains('a'); +verify.completionListContains('b'); + +goTo.marker('4'); +verify.not.completionListContains('x'); +verify.not.completionListContains('y'); +verify.completionListContains('a'); +verify.completionListContains('b'); + +goTo.marker('5'); +verify.not.completionListContains('x'); +verify.not.completionListContains('y'); +verify.completionListContains('a'); +verify.completionListContains('b'); diff --git a/tests/cases/fourslash/javaScriptModules13.ts b/tests/cases/fourslash/javaScriptModules13.ts new file mode 100644 index 0000000000000..8b22971009cf4 --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules13.ts @@ -0,0 +1,26 @@ +/// + +// Assignments to 'module.exports' create an external module + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// if (true) { +//// module.exports = { a: 10 }; +//// } +//// var invisible = true; + +// @Filename: isGlobal.js +//// var y = 10; + +// @Filename: consumer.js +//// var x = require('myMod'); +//// /**/; + +goTo.file('consumer.js'); +goTo.marker(); + +verify.completionListContains('y'); +verify.not.completionListContains('invisible'); + +edit.insert('x.'); +verify.completionListContains('a'); diff --git a/tests/cases/fourslash/javaScriptModules14.ts b/tests/cases/fourslash/javaScriptModules14.ts new file mode 100644 index 0000000000000..5434f94cf8de6 --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules14.ts @@ -0,0 +1,28 @@ +/// + +// Assignments to 'exports.p' stop global variables from being visible in other files + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// if (true) { +//// exports.b = true; +//// } else { +//// exports.n = 3; +//// } +//// function fn() { +//// exports.s = 'foo'; +//// } +//// var invisible = true; + +// @Filename: isGlobal.js +//// var y = 10; + +// @Filename: consumer.js +//// var x = require('myMod'); +//// /**/; + +goTo.file('consumer.js'); +goTo.marker(); + +verify.completionListContains('y'); +verify.not.completionListContains('invisible'); diff --git a/tests/cases/fourslash/javaScriptModules15.ts b/tests/cases/fourslash/javaScriptModules15.ts new file mode 100644 index 0000000000000..ff888d993367f --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules15.ts @@ -0,0 +1,27 @@ +/// + +// Assignments to 'exports.p' define a property 'p' even if they're not at top-level + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// if (true) { +//// exports.b = true; +//// } else { +//// exports.n = 3; +//// } +//// function fn() { +//// exports.s = 'foo'; +//// } + +// @Filename: consumer.js +//// var x = require('myMod'); +//// x/**/; + +goTo.file('consumer.js'); +goTo.marker(); +edit.insert('.'); +verify.completionListContains("n", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("s", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("b", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +edit.insert('n.'); +verify.completionListContains("toFixed", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); diff --git a/tests/cases/fourslash/javaScriptModules16.ts b/tests/cases/fourslash/javaScriptModules16.ts new file mode 100644 index 0000000000000..c556f288145cb --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules16.ts @@ -0,0 +1,22 @@ +/// + +// Assignments to 'exports.p' define a property 'p' + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// exports.n = 3; +//// exports.s = 'foo'; +//// exports.b = true; + +// @Filename: consumer.js +//// var x = require('myMod'); +//// x/**/; + +goTo.file('consumer.js'); +goTo.marker(); +edit.insert('.'); +verify.completionListContains("n", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("s", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("b", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +edit.insert('n.'); +verify.completionListContains("toFixed", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); diff --git a/tests/cases/fourslash/javaScriptModules17.ts b/tests/cases/fourslash/javaScriptModules17.ts new file mode 100644 index 0000000000000..8514ed0ee1844 --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules17.ts @@ -0,0 +1,18 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// module.exports = { n: 3, s: 'foo', b: true }; + +// @Filename: consumer.js +//// var x = require('./myMod'); +//// x/**/; + +goTo.file('consumer.js'); +goTo.marker(); +edit.insert('.'); +verify.completionListContains("n", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("s", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +verify.completionListContains("b", /*displayText:*/ undefined, /*documentation*/ undefined, "property"); +edit.insert('n.'); +verify.completionListContains("toFixed", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); diff --git a/tests/cases/fourslash/javaScriptModules18.ts b/tests/cases/fourslash/javaScriptModules18.ts new file mode 100644 index 0000000000000..d3117f86b3f35 --- /dev/null +++ b/tests/cases/fourslash/javaScriptModules18.ts @@ -0,0 +1,14 @@ +/// + +// CommonJS modules should not pollute the global namespace + +// @allowNonTsExtensions: true +// @Filename: myMod.js +//// var x = require('fs'); + +// @Filename: other.js +//// /**/; + +goTo.file('other.js'); + +verify.not.completionListContains('x'); diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index a76428a62a11e..c53cad5725abc 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -95,8 +95,8 @@ module ts { let resolution = nodeModuleNameResolver(moduleName, containingFile.name, createModuleResolutionHost(containingFile, packageJson, moduleFile)); assert.equal(resolution.resolvedModule.resolvedFileName, moduleFile.name); assert.equal(!!resolution.resolvedModule.isExternalLibraryImport, false); - // expect three failed lookup location - attempt to load module as file with all supported extensions - assert.equal(resolution.failedLookupLocations.length, 3); + // expect five failed lookup location - attempt to load module as file with all supported extensions + assert.equal(resolution.failedLookupLocations.length, 5); } it("module name as directory - load from typings", () => { @@ -117,6 +117,8 @@ module ts { "/a/b/foo.ts", "/a/b/foo.tsx", "/a/b/foo.d.ts", + "/a/b/foo.js", + "/a/b/foo.jsx", "/a/b/foo/index.ts", "/a/b/foo/index.tsx", ]); @@ -143,7 +145,7 @@ module ts { "/a/b/c/node_modules/foo/package.json", "/a/b/c/node_modules/foo/index.ts", "/a/b/c/node_modules/foo/index.tsx", - "/a/b/c/node_modules/foo/index.d.ts" + "/a/b/c/node_modules/foo/index.d.ts", ]) }); diff --git a/tests/cases/unittests/services/preProcessFile.ts b/tests/cases/unittests/services/preProcessFile.ts index a3f8db5528fba..d9ddaf0f256b5 100644 --- a/tests/cases/unittests/services/preProcessFile.ts +++ b/tests/cases/unittests/services/preProcessFile.ts @@ -2,8 +2,8 @@ /// describe('PreProcessFile:', function () { - function test(sourceText: string, readImportFile: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { - var resultPreProcess = ts.preProcessFile(sourceText, readImportFile); + function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { + var resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); var resultIsLibFile = resultPreProcess.isLibFile; var resultImportedFiles = resultPreProcess.importedFiles; @@ -45,7 +45,9 @@ describe('PreProcessFile:', function () { } describe("Test preProcessFiles,", function () { it("Correctly return referenced files from triple slash", function () { - test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", true, + test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [{ fileName: "refFile1.ts", pos: 0, end: 37 }, { fileName: "refFile2.ts", pos: 38, end: 73 }, { fileName: "refFile3.ts", pos: 74, end: 109 }, { fileName: "..\\refFile4d.ts", pos: 110, end: 150 }], @@ -56,7 +58,9 @@ describe('PreProcessFile:', function () { }), it("Do not return reference path because of invalid triple-slash syntax", function () { - test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", true, + test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [], @@ -66,7 +70,9 @@ describe('PreProcessFile:', function () { }), it("Correctly return imported files", function () { - test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", true, + test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [{ fileName: "r1.ts", pos: 20, end: 25 }, { fileName: "r2.ts", pos: 49, end: 54 }, { fileName: "r3.ts", pos: 78, end: 83 }, @@ -77,7 +83,9 @@ describe('PreProcessFile:', function () { }), it("Do not return imported files if readImportFiles argument is false", function () { - test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", false, + test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", + /* readImports */ false, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [], @@ -87,7 +95,9 @@ describe('PreProcessFile:', function () { }), it("Do not return import path because of invalid import syntax", function () { - test("import i1 require(\"r1.ts\"); import = require(\"r2.ts\") import i3= require(\"r3.ts\"); import i5", true, + test("import i1 require(\"r1.ts\"); import = require(\"r2.ts\") import i3= require(\"r3.ts\"); import i5", + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [{ fileName: "r3.ts", pos: 73, end: 78 }], @@ -97,7 +107,9 @@ describe('PreProcessFile:', function () { }), it("Correctly return referenced files and import files", function () { - test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\");", true, + test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\");", + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [{ fileName: "refFile1.ts", pos: 0, end: 35 }, { fileName: "refFile2.ts", pos: 36, end: 71 }], importedFiles: [{ fileName: "r1.ts", pos: 92, end: 97 }, { fileName: "r2.ts", pos: 121, end: 126 }], @@ -107,7 +119,9 @@ describe('PreProcessFile:', function () { }), it("Correctly return referenced files and import files even with some invalid syntax", function () { - test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import = require(\"r2.ts\"); import i2 = require(\"r3.ts\");", true, + test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import = require(\"r2.ts\"); import i2 = require(\"r3.ts\");", + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [{ fileName: "refFile1.ts", pos: 0, end: 35 }], importedFiles: [{ fileName: "r1.ts", pos: 91, end: 96 }, { fileName: "r3.ts", pos: 148, end: 153 }], @@ -124,7 +138,8 @@ describe('PreProcessFile:', function () { "import {a as A} from \"m5\";" + "\n" + "import {a as A, b, c as C} from \"m6\";" + "\n" + "import def , {a, b, c as C} from \"m7\";" + "\n", - true, + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [ @@ -146,7 +161,8 @@ describe('PreProcessFile:', function () { "export {a} from \"m2\";" + "\n" + "export {a as A} from \"m3\";" + "\n" + "export {a as A, b, c as C} from \"m4\";" + "\n", - true, + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [ @@ -166,7 +182,11 @@ describe('PreProcessFile:', function () { declare module "B" {} function foo() { } - `, false, { + `, + /* readImports */ false, + /* detectJavaScriptImports */ false, + + { referencedFiles: [], importedFiles: [], ambientExternalModules: ["B"], @@ -176,7 +196,8 @@ describe('PreProcessFile:', function () { it("Correctly handles export import declarations", function () { test("export import a = require(\"m1\");", - true, + /* readImports */true, + /* detectJavaScriptImports */ false, { referencedFiles: [], importedFiles: [ @@ -186,7 +207,61 @@ describe('PreProcessFile:', function () { isLibFile: false }) }); - + it("Correctly handles export require calls in JavaScript files", function () { + test(` + export import a = require("m1"); + var x = require('m2'); + foo(require('m3')); + var z = { f: require('m4') } + `, + /* readImports */true, + /* detectJavaScriptImports */ true, + { + referencedFiles: [], + importedFiles: [ + { fileName: "m1", pos: 39, end: 41 }, + { fileName: "m2", pos: 74, end: 76 }, + { fileName: "m3", pos: 105, end: 107 }, + { fileName: "m4", pos: 146, end: 148 }, + ], + ambientExternalModules: undefined, + isLibFile: false + }) + }); + it("Correctly handles dependency lists in define([deplist]) calls in JavaScript files", function () { + test(` + define(["mod1", "mod2"], (m1, m2) => { + }); + `, + /* readImports */true, + /* detectJavaScriptImports */ true, + { + referencedFiles: [], + importedFiles: [ + { fileName: "mod1", pos: 21, end: 25 }, + { fileName: "mod2", pos: 29, end: 33 }, + ], + ambientExternalModules: undefined, + isLibFile: false + }) + }); + it("Correctly handles dependency lists in define(modName, [deplist]) calls in JavaScript files", function () { + test(` + define("mod", ["mod1", "mod2"], (m1, m2) => { + }); + `, + /* readImports */true, + /* detectJavaScriptImports */ true, + { + referencedFiles: [], + importedFiles: [ + { fileName: "mod1", pos: 28, end: 32 }, + { fileName: "mod2", pos: 36, end: 40 }, + ], + ambientExternalModules: undefined, + isLibFile: false + }) + }); }); }); diff --git a/tests/webTestServer.ts b/tests/webTestServer.ts index 6ddc18f1344cf..6bd15359e3f6b 100644 --- a/tests/webTestServer.ts +++ b/tests/webTestServer.ts @@ -5,7 +5,7 @@ import fs = require("fs"); import path = require("path"); import url = require("url"); import child_process = require("child_process"); -import os = require('os'); +import os = require("os"); /// Command line processing /// @@ -276,7 +276,6 @@ if ((browser && browser === 'chrome')) { console.log(`default Chrome location is unknown for platform '${os.platform()}'`); break; } - if (fs.existsSync(defaultChromePath)) { browserPath = defaultChromePath; } else {