diff --git a/.travis.yml b/.travis.yml index 20b47dbe30c..0f720b7375e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,11 +16,7 @@ matrix: branches: only: - master - - release-2.7 - - release-2.8 - - release-2.9 - - release-3.0 - - release-3.1 + - /^release-.*/ install: - npm uninstall typescript --no-save diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbce8186fac..683c47817b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,19 +82,26 @@ Your pull request should: * To avoid line ending issues, set `autocrlf = input` and `whitespace = cr-at-eol` in your git configuration ## Contributing `lib.d.ts` fixes - -The library sources are in: [src/lib](https://github.com/Microsoft/TypeScript/tree/master/src/lib) -Library files in `built/local/` are updated by running -```Shell +There are three relevant locations to be aware of when it comes to TypeScript's library declaration files: + +* `src/lib`: the location of the sources themselves. +* `lib`: the location of the last-known-good (LKG) versions of the files which are updated periodically. +* `built/local`: the build output location, including where `src/lib` files will be copied to. + +Any changes should be made to [src/lib](https://github.com/Microsoft/TypeScript/tree/master/src/lib). **Most** of these files can be updated by hand, with the exception of any generated files (see below). + +Library files in `built/local/` are updated automatically by running the standard build task: + +```sh jake ``` -The files in `lib/` are used to bootstrap compilation and usually do not need to be updated. +The files in `lib/` are used to bootstrap compilation and usually **should not** be updated unless publishing a new version or updating the LKG. -#### `src/lib/dom.generated.d.ts` and `src/lib/webworker.generated.d.ts` +### Modifying generated library files -These two files represent the DOM typings and are auto-generated. To make any modifications to them, please submit a PR to https://github.com/Microsoft/TSJS-lib-generator +The files `src/lib/dom.generated.d.ts` and `src/lib/webworker.generated.d.ts` both represent type declarations for the DOM and are auto-generated. To make any modifications to them, you will have to direct changes to https://github.com/Microsoft/TSJS-lib-generator ## Running the Tests diff --git a/package.json b/package.json index 3f5cb1223e9..d8c514363e5 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "mocha": "latest", "mocha-fivemat-progress-reporter": "latest", "plugin-error": "latest", + "pretty-hrtime": "^1.0.3", "prex": "^0.4.3", "q": "latest", "remove-internal": "^2.9.2", diff --git a/scripts/build/project.js b/scripts/build/project.js index 0375faa2820..6c1ac0f3e04 100644 --- a/scripts/build/project.js +++ b/scripts/build/project.js @@ -683,11 +683,17 @@ function ensureCompileTask(projectGraph, options) { } }); } - const js = (projectGraphConfig.resolvedOptions.js ? projectGraphConfig.resolvedOptions.js(stream.js) : stream.js) + + const additionalJsOutputs = projectGraphConfig.resolvedOptions.js ? projectGraphConfig.resolvedOptions.js(stream.js) + .pipe(gulpif(sourceMap || inlineSourceMap, sourcemaps.write(sourceMapPath, sourceMapOptions))) : undefined; + const additionalDtsOutputs = projectGraphConfig.resolvedOptions.dts ? projectGraphConfig.resolvedOptions.dts(stream.dts) + .pipe(gulpif(declarationMap, sourcemaps.write(sourceMapPath, sourceMapOptions))) : undefined; + + const js = stream.js .pipe(gulpif(sourceMap || inlineSourceMap, sourcemaps.write(sourceMapPath, sourceMapOptions))); - const dts = (projectGraphConfig.resolvedOptions.dts ? projectGraphConfig.resolvedOptions.dts(stream.dts) : stream.dts) + const dts = stream.dts .pipe(gulpif(declarationMap, sourcemaps.write(sourceMapPath, sourceMapOptions))); - return merge2([js, dts]) + return merge2([js, dts, additionalJsOutputs, additionalDtsOutputs].filter(x => !!x)) .pipe(gulp.dest(destPath)); }); } diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 7cb321a95fa..7216af4e817 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -143,7 +143,7 @@ namespace ts { let symbolCount = 0; - let Symbol: { new (flags: SymbolFlags, name: __String): Symbol }; // tslint:disable-line variable-name + let Symbol: new (flags: SymbolFlags, name: __String) => Symbol; // tslint:disable-line variable-name let classifiableNames: UnderscoreEscapedMap; const unreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; @@ -233,6 +233,11 @@ namespace ts { symbol.members = createSymbolTable(); } + // On merge of const enum module with class or function, reset const enum only flag (namespaces will already recalculate) + if (symbol.constEnumOnlyModule && (symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.RegularEnum))) { + symbol.constEnumOnlyModule = false; + } + if (symbolFlags & SymbolFlags.Value) { setValueDeclaration(symbol, node); } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index acf5125aa9b..20e6310f771 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4426,6 +4426,22 @@ namespace ts { return location.kind === SyntaxKind.SourceFile || isAmbientModule(location); } + function getNameOfSymbolFromNameType(symbol: Symbol, context?: NodeBuilderContext) { + const nameType = symbol.nameType; + if (nameType) { + if (nameType.flags & TypeFlags.StringOrNumberLiteral) { + const name = "" + (nameType).value; + if (!isIdentifierText(name, compilerOptions.target) && !isNumericLiteralName(name)) { + return `"${escapeString(name, CharacterCodes.doubleQuote)}"`; + } + return name; + } + if (nameType.flags & TypeFlags.UniqueESSymbol) { + return `[${getNameOfSymbolAsWritten((nameType).symbol, context)}]`; + } + } + } + /** * Gets a human-readable name for a symbol. * Should *not* be used for the right-hand side of a `.` -- use `symbolName(symbol)` for that instead. @@ -4450,6 +4466,13 @@ namespace ts { if (isCallExpression(declaration) && isBindableObjectDefinePropertyCall(declaration)) { return symbolName(symbol); } + if (isComputedPropertyName(name) && !(getCheckFlags(symbol) & CheckFlags.Late) && symbol.nameType && symbol.nameType.flags & TypeFlags.StringOrNumberLiteral) { + // Computed property name isn't late bound, but has a well-known name type - use name type to generate a symbol name + const result = getNameOfSymbolFromNameType(symbol, context); + if (result !== undefined) { + return result; + } + } return declarationNameToString(name); } if (declaration.parent && declaration.parent.kind === SyntaxKind.VariableDeclaration) { @@ -4465,16 +4488,8 @@ namespace ts { return declaration.kind === SyntaxKind.ClassExpression ? "(Anonymous class)" : "(Anonymous function)"; } } - const nameType = symbol.nameType; - if (nameType) { - if (nameType.flags & TypeFlags.StringLiteral && !isIdentifierText((nameType).value, compilerOptions.target)) { - return `"${escapeString((nameType).value, CharacterCodes.doubleQuote)}"`; - } - if (nameType && nameType.flags & TypeFlags.UniqueESSymbol) { - return `[${getNameOfSymbolAsWritten((nameType).symbol, context)}]`; - } - } - return symbolName(symbol); + const name = getNameOfSymbolFromNameType(symbol, context); + return name !== undefined ? name : symbolName(symbol); } function isDeclarationVisible(node: Node): boolean { @@ -4768,6 +4783,69 @@ namespace ts { return createAnonymousType(symbol, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); } + // Determine the control flow type associated with a destructuring declaration or assignment. The following + // forms of destructuring are possible: + // let { x } = obj; // BindingElement + // let [ x ] = obj; // BindingElement + // { x } = obj; // ShorthandPropertyAssignment + // { x: v } = obj; // PropertyAssignment + // [ x ] = obj; // Expression + // We construct a synthetic element access expression corresponding to 'obj.x' such that the control + // flow analyzer doesn't have to handle all the different syntactic forms. + function getFlowTypeOfDestructuring(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression, declaredType: Type) { + const reference = getSyntheticElementAccess(node); + return reference ? getFlowTypeOfReference(reference, declaredType) : declaredType; + } + + function getSyntheticElementAccess(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression): ElementAccessExpression | undefined { + const parentAccess = getParentElementAccess(node); + if (parentAccess && parentAccess.flowNode) { + const propName = getDestructuringPropertyName(node); + if (propName) { + const result = createNode(SyntaxKind.ElementAccessExpression, node.pos, node.end); + result.parent = node; + result.expression = parentAccess; + const literal = createNode(SyntaxKind.StringLiteral, node.pos, node.end); + literal.parent = result; + literal.text = propName; + result.argumentExpression = literal; + result.flowNode = parentAccess.flowNode; + return result; + } + } + } + + function getParentElementAccess(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression) { + const ancestor = node.parent.parent; + switch (ancestor.kind) { + case SyntaxKind.BindingElement: + case SyntaxKind.PropertyAssignment: + return getSyntheticElementAccess(ancestor); + case SyntaxKind.ArrayLiteralExpression: + return getSyntheticElementAccess(node.parent); + case SyntaxKind.VariableDeclaration: + return (ancestor).initializer; + case SyntaxKind.BinaryExpression: + return (ancestor).right; + } + } + + function getDestructuringPropertyName(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression) { + const parent = node.parent; + if (node.kind === SyntaxKind.BindingElement && parent.kind === SyntaxKind.ObjectBindingPattern) { + return getLiteralPropertyNameText((node).propertyName || (node).name); + } + if (node.kind === SyntaxKind.PropertyAssignment || node.kind === SyntaxKind.ShorthandPropertyAssignment) { + return getLiteralPropertyNameText((node).name); + } + return "" + (>(parent).elements).indexOf(node); + } + + function getLiteralPropertyNameText(name: PropertyName) { + const type = getLiteralTypeFromPropertyName(name); + return type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral) ? "" + (type).value : undefined; + } + /** Return the inferred type for a binding element */ function getTypeForBindingElement(declaration: BindingElement): Type | undefined { const pattern = declaration.parent; @@ -4808,9 +4886,9 @@ namespace ts { else { // Use explicitly specified property name ({ p: xxx } form), or otherwise the implied name ({ p } form) const name = declaration.propertyName || declaration.name; - const exprType = getLiteralTypeFromPropertyName(name); - const declaredType = checkIndexedAccessIndexType(getIndexedAccessType(parentType, exprType, name), name); - type = getFlowTypeOfReference(declaration, getConstraintForLocation(declaredType, declaration.name)); + const indexType = getLiteralTypeFromPropertyName(name); + const declaredType = getConstraintForLocation(getIndexedAccessType(parentType, indexType, name), declaration.name); + type = getFlowTypeOfDestructuring(declaration, declaredType); } } else { @@ -4827,21 +4905,13 @@ namespace ts { mapType(parentType, t => sliceTupleType(t, index)) : createArrayType(elementType); } + else if (isArrayLikeType(parentType)) { + const indexType = getLiteralType(index); + const declaredType = getConstraintForLocation(getIndexedAccessType(parentType, indexType, declaration.name), declaration.name); + type = getFlowTypeOfDestructuring(declaration, declaredType); + } else { - // Use specific property type when parent is a tuple or numeric index type when parent is an array - const index = pattern.elements.indexOf(declaration); - type = everyType(parentType, isTupleLikeType) ? - getTupleElementType(parentType, index) || declaration.initializer && checkDeclarationInitializer(declaration) : - elementType; - if (!type) { - if (isTupleType(parentType)) { - error(declaration, Diagnostics.Tuple_type_0_with_length_1_cannot_be_assigned_to_tuple_with_length_2, typeToString(parentType), getTypeReferenceArity(parentType), pattern.elements.length); - } - else { - error(declaration, Diagnostics.Type_0_has_no_property_1, typeToString(parentType), "" + index); - } - return errorType; - } + type = elementType; } } // In strict null checking mode, if a default value of a non-undefined type is specified, remove @@ -6729,7 +6799,12 @@ namespace ts { // type is the union of the constituent return types. function getUnionSignatures(signatureLists: ReadonlyArray>): Signature[] { let result: Signature[] | undefined; + let indexWithLengthOverOne: number | undefined; for (let i = 0; i < signatureLists.length; i++) { + if (signatureLists[i].length === 0) return emptyArray; + if (signatureLists[i].length > 1) { + indexWithLengthOverOne = indexWithLengthOverOne === undefined ? i : -1; // -1 is a signal there are multiple overload sets + } for (const signature of signatureLists[i]) { // Only process signatures with parameter lists that aren't already in the result list if (!result || !findMatchingSignature(result, signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true)) { @@ -6753,9 +6828,91 @@ namespace ts { } } } + if (!length(result) && indexWithLengthOverOne !== -1) { + // No sufficiently similar signature existed to subsume all the other signatures in the union - time to see if we can make a single + // signature that handles all over them. We only do this when there are overloads in only one constituent. + // (Overloads are conditional in nature and having overloads in multiple constituents would necessitate making a power set of + // signatures from the type, whose ordering would be non-obvious) + const masterList = signatureLists[indexWithLengthOverOne !== undefined ? indexWithLengthOverOne : 0]; + let results: Signature[] | undefined = masterList.slice(); + for (const signatures of signatureLists) { + if (signatures !== masterList) { + const signature = signatures[0]; + Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass"); + results = signature.typeParameters && some(results, s => !!s.typeParameters) ? undefined : map(results, sig => combineSignaturesOfUnionMembers(sig, signature)); + if (!results) { + break; + } + } + } + result = results; + } return result || emptyArray; } + function combineUnionThisParam(left: Symbol | undefined, right: Symbol | undefined): Symbol | undefined { + if (!left || !right) { + return left || right; + } + // A signature `this` type might be a read or a write position... It's very possible that it should be invariant + // and we should refuse to merge signatures if there are `this` types and they do not match. However, so as to be + // permissive when calling, for now, we'll union the `this` types just like the overlapping-union-signature check does + const thisType = getUnionType([getTypeOfSymbol(left), getTypeOfSymbol(right)], UnionReduction.Subtype); + return createSymbolWithType(left, thisType); + } + + function combineUnionParameters(left: Signature, right: Signature) { + const longest = getParameterCount(left) >= getParameterCount(right) ? left : right; + const shorter = longest === left ? right : left; + const longestCount = getParameterCount(longest); + const eitherHasEffectiveRest = (hasEffectiveRestParameter(left) || hasEffectiveRestParameter(right)); + const needsExtraRestElement = eitherHasEffectiveRest && !hasEffectiveRestParameter(longest); + const params = new Array(longestCount + (needsExtraRestElement ? 1 : 0)); + for (let i = 0; i < longestCount; i++) { + const longestParamType = tryGetTypeAtPosition(longest, i)!; + const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; + const unionParamType = getIntersectionType([longestParamType, shorterParamType]); + const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1); + const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter); + const leftName = getParameterNameAtPosition(left, i); + const rightName = getParameterNameAtPosition(right, i); + const paramSymbol = createSymbol( + SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0), + leftName === rightName ? leftName : `arg${i}` as __String + ); + paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; + params[i] = paramSymbol; + } + if (needsExtraRestElement) { + const restParamSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "args" as __String); + restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); + params[longestCount] = restParamSymbol; + } + return params; + } + + function combineSignaturesOfUnionMembers(left: Signature, right: Signature): Signature { + const declaration = left.declaration; + const params = combineUnionParameters(left, right); + const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter); + const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); + const hasRestParam = left.hasRestParameter || right.hasRestParameter; + const hasLiteralTypes = left.hasLiteralTypes || right.hasLiteralTypes; + const result = createSignature( + declaration, + left.typeParameters || right.typeParameters, + thisParam, + params, + /*resolvedReturnType*/ undefined, + /*resolvedTypePredicate*/ undefined, + minArgCount, + hasRestParam, + hasLiteralTypes + ); + result.unionSignatures = concatenate(left.unionSignatures || [left], [right]); + return result; + } + function getUnionIndexInfo(types: ReadonlyArray, kind: IndexKind): IndexInfo | undefined { const indexTypes: Type[] = []; let isAnyReadonly = false; @@ -6927,6 +7084,39 @@ namespace ts { setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, undefined); } + // Return the lower bound of the key type in a mapped type. Intuitively, the lower + // bound includes those keys that are known to always be present, for example because + // because of constraints on type parameters (e.g. 'keyof T' for a constrained T). + function getLowerBoundOfKeyType(type: Type): Type { + if (type.flags & (TypeFlags.Any | TypeFlags.Primitive)) { + return type; + } + if (type.flags & TypeFlags.Index) { + return getIndexType(getApparentType((type).type)); + } + if (type.flags & TypeFlags.Conditional) { + return getLowerBoundOfConditionalType(type); + } + if (type.flags & TypeFlags.Union) { + return getUnionType(sameMap((type).types, getLowerBoundOfKeyType)); + } + if (type.flags & TypeFlags.Intersection) { + return getIntersectionType(sameMap((type).types, getLowerBoundOfKeyType)); + } + return neverType; + } + + function getLowerBoundOfConditionalType(type: ConditionalType) { + if (type.root.isDistributive) { + const constraint = getLowerBoundOfKeyType(type.checkType); + if (constraint !== type.checkType) { + const mapper = makeUnaryTypeMapper(type.root.checkType, constraint); + return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper)); + } + } + return type; + } + /** Resolve the members of a mapped type { [P in K]: T } */ function resolveMappedTypeMembers(type: MappedType) { const members: SymbolTable = createSymbolTable(); @@ -6955,10 +7145,7 @@ namespace ts { } } else { - // If the key type is a 'keyof X', obtain 'keyof C' where C is the base constraint of X. - // Then iterate over the constituents of the key type. - const iterationType = constraintType.flags & TypeFlags.Index ? getIndexType(getApparentType((constraintType).type)) : constraintType; - forEachType(iterationType, addMemberForKeyType); + forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); } setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); @@ -7158,7 +7345,7 @@ namespace ts { return list.some(property => { const name = property.name && getTextOfPropertyName(property.name); const expected = name === undefined ? undefined : getTypeOfPropertyOfType(contextualType, name); - return !!expected && typeIsLiteralType(expected) && !isTypeIdenticalTo(getTypeOfNode(property), expected); + return !!expected && isLiteralType(expected) && !isTypeIdenticalTo(getTypeOfNode(property), expected); }); } @@ -7512,38 +7699,37 @@ namespace ts { return props[0]; } let declarations: Declaration[] | undefined; - let commonType: Type | undefined; + let firstType: Type | undefined; let nameType: Type | undefined; const propTypes: Type[] = []; - let first = true; - let commonValueDeclaration: Declaration | undefined; + let firstValueDeclaration: Declaration | undefined; let hasNonUniformValueDeclaration = false; for (const prop of props) { - if (!commonValueDeclaration) { - commonValueDeclaration = prop.valueDeclaration; + if (!firstValueDeclaration) { + firstValueDeclaration = prop.valueDeclaration; } - else if (prop.valueDeclaration !== commonValueDeclaration) { + else if (prop.valueDeclaration !== firstValueDeclaration) { hasNonUniformValueDeclaration = true; } declarations = addRange(declarations, prop.declarations); const type = getTypeOfSymbol(prop); - if (first) { - commonType = type; + if (!firstType) { + firstType = type; nameType = prop.nameType; - first = false; } - else { - if (type !== commonType) { - checkFlags |= CheckFlags.HasNonUniformType; - } + else if (type !== firstType) { + checkFlags |= CheckFlags.HasNonUniformType; + } + if (isLiteralType(type)) { + checkFlags |= CheckFlags.HasLiteralType; } propTypes.push(type); } addRange(propTypes, indexTypes); const result = createSymbol(SymbolFlags.Property | commonFlags, name, syntheticFlag | checkFlags); result.containingType = containingType; - if (!hasNonUniformValueDeclaration && commonValueDeclaration) { - result.valueDeclaration = commonValueDeclaration; + if (!hasNonUniformValueDeclaration && firstValueDeclaration) { + result.valueDeclaration = firstValueDeclaration; } result.declarations = declarations!; result.nameType = nameType; @@ -7771,22 +7957,17 @@ namespace ts { const numTypeArguments = length(typeArguments); if (isJavaScriptImplicitAny || (numTypeArguments >= minTypeArgumentCount && numTypeArguments <= numTypeParameters)) { const result = typeArguments ? typeArguments.slice() : []; - - // Map an unsatisfied type parameter with a default type. - // If a type parameter does not have a default type, or if the default type - // is a forward reference, the empty object type is used. - const baseDefaultType = getDefaultTypeArgumentType(isJavaScriptImplicitAny); - const circularityMapper = createTypeMapper(typeParameters!, map(typeParameters!, () => baseDefaultType)); + // Map invalid forward references in default types to the error type for (let i = numTypeArguments; i < numTypeParameters; i++) { - result[i] = instantiateType(getConstraintFromTypeParameter(typeParameters![i]) || baseDefaultType, circularityMapper); + result[i] = errorType; } + const baseDefaultType = getDefaultTypeArgumentType(isJavaScriptImplicitAny); for (let i = numTypeArguments; i < numTypeParameters; i++) { - const mapper = createTypeMapper(typeParameters!, result); let defaultType = getDefaultFromTypeParameter(typeParameters![i]); if (isJavaScriptImplicitAny && defaultType && isTypeIdenticalTo(defaultType, emptyObjectType)) { defaultType = anyType; } - result[i] = defaultType ? instantiateType(defaultType, mapper) : baseDefaultType; + result[i] = defaultType ? instantiateType(defaultType, createTypeMapper(typeParameters!, result)) : baseDefaultType; } result.length = typeParameters!.length; return result; @@ -9523,16 +9704,16 @@ namespace ts { return false; } - function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | undefined, cacheSymbol: boolean, missingType: Type) { + function getPropertyTypeForIndexType(objectType: Type, indexType: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression | undefined, cacheSymbol: boolean, missingType: Type) { const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; - const propName = isTypeUsableAsLateBoundName(indexType) - ? getLateBoundNameFromType(indexType) - : accessExpression && checkThatExpressionIsProperSymbolReference(accessExpression.argumentExpression, indexType, /*reportError*/ false) - ? getPropertyNameForKnownSymbolName(idText((accessExpression.argumentExpression).name)) - : accessNode && isPropertyName(accessNode) + const propName = isTypeUsableAsLateBoundName(indexType) ? + getLateBoundNameFromType(indexType) : + accessExpression && checkThatExpressionIsProperSymbolReference(accessExpression.argumentExpression, indexType, /*reportError*/ false) ? + getPropertyNameForKnownSymbolName(idText((accessExpression.argumentExpression).name)) : + accessNode && isPropertyName(accessNode) ? // late bound names are handled in the first branch, so here we only need to handle normal names - ? getPropertyNameForPropertyNameNode(accessNode) - : undefined; + getPropertyNameForPropertyNameNode(accessNode) : + undefined; if (propName !== undefined) { const prop = getPropertyOfType(objectType, propName); if (prop) { @@ -9554,7 +9735,13 @@ namespace ts { if (everyType(objectType, isTupleType) && isNumericLiteralName(propName) && +propName >= 0) { if (accessNode && everyType(objectType, t => !(t).target.hasRestElement)) { const indexNode = getIndexNodeForAccessExpression(accessNode); - error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); + if (isTupleType(objectType)) { + error(indexNode, Diagnostics.Tuple_type_0_of_length_1_has_no_element_at_index_2, + typeToString(objectType), getTypeReferenceArity(objectType), unescapeLeadingUnderscores(propName)); + } + else { + error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); + } } return mapType(objectType, t => getRestTypeOfTupleType(t) || undefinedType); } @@ -9626,7 +9813,7 @@ namespace ts { return missingType; } - function getIndexNodeForAccessExpression(accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName) { + function getIndexNodeForAccessExpression(accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression) { return accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode.argumentExpression : accessNode.kind === SyntaxKind.IndexedAccessType @@ -9695,7 +9882,7 @@ namespace ts { return type.simplified = type; } - function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName, missingType = accessNode ? errorType : unknownType): Type { + function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression, missingType = accessNode ? errorType : unknownType): Type { if (objectType === wildcardType || indexType === wildcardType) { return wildcardType; } @@ -11386,7 +11573,7 @@ namespace ts { } function isEmptyObjectType(type: Type): boolean { - return type.flags & TypeFlags.Object ? isEmptyResolvedType(resolveStructuredTypeMembers(type)) : + return type.flags & TypeFlags.Object ? !isGenericMappedType(type) && isEmptyResolvedType(resolveStructuredTypeMembers(type)) : type.flags & TypeFlags.NonPrimitive ? true : type.flags & TypeFlags.Union ? some((type).types, isEmptyObjectType) : type.flags & TypeFlags.Intersection ? every((type).types, isEmptyObjectType) : @@ -12361,7 +12548,7 @@ namespace ts { } else { // An empty object type is related to any mapped type that includes a '?' modifier. - if (isPartialMappedType(target) && !isGenericMappedType(source) && isEmptyObjectType(source)) { + if (isPartialMappedType(target) && isEmptyObjectType(source)) { return Ternary.True; } if (isGenericMappedType(target)) { @@ -13597,6 +13784,9 @@ namespace ts { // union includes empty object types (e.g. reducing {} | string to just {}). return getUnionType(widenedTypes, some(widenedTypes, isEmptyObjectType) ? UnionReduction.Subtype : UnionReduction.Literal); } + if (type.flags & TypeFlags.Intersection) { + return getIntersectionType(sameMap((type).types, getWidenedType)); + } if (isArrayType(type) || isTupleType(type)) { return createTypeReference((type).target, sameMap((type).typeArguments, getWidenedType)); } @@ -14569,49 +14759,23 @@ namespace ts { // occurring in an apparent type position with '@' because the control flow type // of such nodes may be based on the apparent type instead of the declared type. function getFlowCacheKey(node: Node): string | undefined { - if (node.kind === SyntaxKind.Identifier) { - const symbol = getResolvedSymbol(node); - return symbol !== unknownSymbol ? (isConstraintPosition(node) ? "@" : "") + getSymbolId(symbol) : undefined; - } - if (node.kind === SyntaxKind.ThisKeyword) { - return "0"; - } - if (node.kind === SyntaxKind.PropertyAccessExpression) { - const key = getFlowCacheKey((node).expression); - return key && key + "." + idText((node).name); - } - if (node.kind === SyntaxKind.BindingElement) { - const container = (node as BindingElement).parent.parent; - const key = container.kind === SyntaxKind.BindingElement ? getFlowCacheKey(container) : (container.initializer && getFlowCacheKey(container.initializer)); - const text = getBindingElementNameText(node as BindingElement); - const result = key && text && (key + "." + text); - return result; + switch (node.kind) { + case SyntaxKind.Identifier: + const symbol = getResolvedSymbol(node); + return symbol !== unknownSymbol ? (isConstraintPosition(node) ? "@" : "") + getSymbolId(symbol) : undefined; + case SyntaxKind.ThisKeyword: + return "0"; + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + const propName = getAccessedPropertyName(node); + if (propName !== undefined) { + const key = getFlowCacheKey((node).expression); + return key && key + "." + propName; + } } return undefined; } - function getBindingElementNameText(element: BindingElement): string | undefined { - const parent = element.parent; - if (parent.kind === SyntaxKind.ObjectBindingPattern) { - const name = element.propertyName || element.name; - switch (name.kind) { - case SyntaxKind.Identifier: - return idText(name); - case SyntaxKind.ComputedPropertyName: - return isStringOrNumericLiteralLike(name.expression) ? name.expression.text : undefined; - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - return name.text; - default: - // Per types, array and object binding patterns remain, however they should never be present if propertyName is not defined - Debug.fail("Unexpected name kind for binding element name"); - } - } - else { - return "" + parent.elements.indexOf(element); - } - } - function isMatchingReference(source: Node, target: Node): boolean { switch (source.kind) { case SyntaxKind.Identifier: @@ -14624,49 +14788,27 @@ namespace ts { return target.kind === SyntaxKind.SuperKeyword; case SyntaxKind.PropertyAccessExpression: case SyntaxKind.ElementAccessExpression: - return (isPropertyAccessExpression(target) || isElementAccessExpression(target)) && - getAccessedPropertyName(source as PropertyAccessExpression | ElementAccessExpression) === getAccessedPropertyName(target) && - isMatchingReference((source as PropertyAccessExpression | ElementAccessExpression).expression, target.expression); - case SyntaxKind.BindingElement: - if (target.kind === SyntaxKind.PropertyAccessExpression && (target).name.escapedText === getBindingElementNameText(source)) { - const ancestor = source.parent.parent; - if (ancestor.kind === SyntaxKind.BindingElement) { - return isMatchingReference(ancestor, (target).expression); - } - if (ancestor.kind === SyntaxKind.VariableDeclaration) { - const initializer = (ancestor).initializer; - return !!initializer && isMatchingReference(initializer, (target).expression); - } - } + return isAccessExpression(target) && + getAccessedPropertyName(source) === getAccessedPropertyName(target) && + isMatchingReference((source).expression, target.expression); } return false; } - function getAccessedPropertyName(access: PropertyAccessExpression | ElementAccessExpression): __String | undefined { - return isPropertyAccessExpression(access) ? access.name.escapedText : + function getAccessedPropertyName(access: AccessExpression): __String | undefined { + return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText : isStringLiteral(access.argumentExpression) || isNumericLiteral(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) : undefined; } - function getReferenceParent(source: Node) { - if (source.kind === SyntaxKind.PropertyAccessExpression) { - return (source).expression; - } - if (source.kind === SyntaxKind.BindingElement) { - const ancestor = source.parent.parent; - return ancestor.kind === SyntaxKind.VariableDeclaration ? (ancestor).initializer : ancestor; - } - return undefined; - } - function containsMatchingReference(source: Node, target: Node) { - let parent = getReferenceParent(source); - while (parent) { - if (isMatchingReference(parent, target)) { + while (isAccessExpression(source)) { + source = source.expression; + if (isMatchingReference(source, target)) { return true; } - parent = getReferenceParent(parent); } + return false; } // Return true if target is a property access xxx.yyy, source is a property access xxx.zzz, the declared @@ -14674,34 +14816,30 @@ namespace ts { // a possible discriminant if its type differs in the constituents of containing union type, and if every // choice is a unit type or a union of unit types. function containsMatchingReferenceDiscriminant(source: Node, target: Node) { - return target.kind === SyntaxKind.PropertyAccessExpression && - containsMatchingReference(source, (target).expression) && - isDiscriminantProperty(getDeclaredTypeOfReference((target).expression), (target).name.escapedText); + let name; + return isAccessExpression(target) && + containsMatchingReference(source, target.expression) && + (name = getAccessedPropertyName(target)) !== undefined && + isDiscriminantProperty(getDeclaredTypeOfReference(target.expression), name); } function getDeclaredTypeOfReference(expr: Node): Type | undefined { if (expr.kind === SyntaxKind.Identifier) { return getTypeOfSymbol(getResolvedSymbol(expr)); } - if (expr.kind === SyntaxKind.PropertyAccessExpression) { - const type = getDeclaredTypeOfReference((expr).expression); - return type && getTypeOfPropertyOfType(type, (expr).name.escapedText); + if (isAccessExpression(expr)) { + const type = getDeclaredTypeOfReference(expr.expression); + if (type) { + const propName = getAccessedPropertyName(expr); + return propName !== undefined ? getTypeOfPropertyOfType(type, propName) : undefined; + } } return undefined; } function isDiscriminantType(type: Type): boolean { - if (type.flags & TypeFlags.Union) { - if (type.flags & (TypeFlags.Boolean | TypeFlags.EnumLiteral)) { - return true; - } - let combined = 0; - for (const t of (type).types) combined |= t.flags; - if (combined & TypeFlags.Unit && !(combined & TypeFlags.Instantiable)) { - return true; - } - } - return false; + return !!(type.flags & TypeFlags.Union && + (type.flags & (TypeFlags.Boolean | TypeFlags.EnumLiteral) || !isGenericIndexType(type))); } function isDiscriminantProperty(type: Type | undefined, name: __String) { @@ -14709,7 +14847,9 @@ namespace ts { const prop = getUnionOrIntersectionProperty(type, name); if (prop && getCheckFlags(prop) & CheckFlags.SyntheticProperty) { if ((prop).isDiscriminantProperty === undefined) { - (prop).isDiscriminantProperty = !!((prop).checkFlags & CheckFlags.HasNonUniformType) && isDiscriminantType(getTypeOfSymbol(prop)); + (prop).isDiscriminantProperty = + ((prop).checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant && + isDiscriminantType(getTypeOfSymbol(prop)); } return !!(prop).isDiscriminantProperty; } @@ -15511,7 +15651,7 @@ namespace ts { else if (isMatchingReferenceDiscriminant(expr, type)) { type = narrowTypeByDiscriminant( type, - expr as PropertyAccessExpression | ElementAccessExpression, + expr as AccessExpression, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd)); } else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) { @@ -15631,21 +15771,19 @@ namespace ts { } function isMatchingReferenceDiscriminant(expr: Expression, computedType: Type) { - if (!(computedType.flags & TypeFlags.Union) || - expr.kind !== SyntaxKind.PropertyAccessExpression && expr.kind !== SyntaxKind.ElementAccessExpression) { + if (!(computedType.flags & TypeFlags.Union) || !isAccessExpression(expr)) { return false; } - const access = expr as PropertyAccessExpression | ElementAccessExpression; - const name = getAccessedPropertyName(access); - if (!name) { + const name = getAccessedPropertyName(expr); + if (name === undefined) { return false; } - return isMatchingReference(reference, access.expression) && isDiscriminantProperty(computedType, name); + return isMatchingReference(reference, expr.expression) && isDiscriminantProperty(computedType, name); } - function narrowTypeByDiscriminant(type: Type, access: PropertyAccessExpression | ElementAccessExpression, narrowType: (t: Type) => Type): Type { + function narrowTypeByDiscriminant(type: Type, access: AccessExpression, narrowType: (t: Type) => Type): Type { const propName = getAccessedPropertyName(access); - if (!propName) { + if (propName === undefined) { return type; } const propType = getTypeOfPropertyOfType(type, propName); @@ -15658,7 +15796,7 @@ namespace ts { return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); } if (isMatchingReferenceDiscriminant(expr, declaredType)) { - return narrowTypeByDiscriminant(type, expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); + return narrowTypeByDiscriminant(type, expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); } if (containsMatchingReferenceDiscriminant(reference, expr)) { return declaredType; @@ -15709,10 +15847,10 @@ namespace ts { return narrowTypeByEquality(type, operator, left, assumeTrue); } if (isMatchingReferenceDiscriminant(left, declaredType)) { - return narrowTypeByDiscriminant(type, left, t => narrowTypeByEquality(t, operator, right, assumeTrue)); + return narrowTypeByDiscriminant(type, left, t => narrowTypeByEquality(t, operator, right, assumeTrue)); } if (isMatchingReferenceDiscriminant(right, declaredType)) { - return narrowTypeByDiscriminant(type, right, t => narrowTypeByEquality(t, operator, left, assumeTrue)); + return narrowTypeByDiscriminant(type, right, t => narrowTypeByEquality(t, operator, left, assumeTrue)); } if (containsMatchingReferenceDiscriminant(reference, left) || containsMatchingReferenceDiscriminant(reference, right)) { return declaredType; @@ -16017,9 +16155,8 @@ namespace ts { } else { const invokedExpression = skipParentheses(callExpression.expression); - if (invokedExpression.kind === SyntaxKind.ElementAccessExpression || invokedExpression.kind === SyntaxKind.PropertyAccessExpression) { - const accessExpression = invokedExpression as ElementAccessExpression | PropertyAccessExpression; - const possibleReference = skipParentheses(accessExpression.expression); + if (isAccessExpression(invokedExpression)) { + const possibleReference = skipParentheses(invokedExpression.expression); if (isMatchingReference(reference, possibleReference)) { return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf); } @@ -16962,7 +17099,7 @@ namespace ts { if (parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.EqualsToken) { const target = (parent).left; if (target.kind === SyntaxKind.PropertyAccessExpression || target.kind === SyntaxKind.ElementAccessExpression) { - const { expression } = target as PropertyAccessExpression | ElementAccessExpression; + const { expression } = target as AccessExpression; // Don't contextually type `this` as `exports` in `exports.Point = function(x, y) { this.x = x; this.y = y; }` if (inJs && isIdentifier(expression)) { const sourceFile = getSourceFileOfNode(parent); @@ -17009,6 +17146,32 @@ namespace ts { } } + function getContextualTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration): Type | undefined { + const typeNode = getEffectiveTypeAnnotationNode(declaration); + if (typeNode) { + return getTypeFromTypeNode(typeNode); + } + switch (declaration.kind) { + case SyntaxKind.Parameter: + return getContextuallyTypedParameterType(declaration); + case SyntaxKind.BindingElement: + return getContextualTypeForBindingElement(declaration); + // By default, do nothing and return undefined - only parameters and binding elements have context implied by a parent + } + } + + function getContextualTypeForBindingElement(declaration: BindingElement): Type | undefined { + const parentDeclaration = declaration.parent.parent; + const name = declaration.propertyName || declaration.name; + const parentType = getContextualTypeForVariableLikeDeclaration(parentDeclaration); + if (parentType && !isBindingPattern(name)) { + const text = getTextOfPropertyName(name); + if (text !== undefined) { + return getTypeOfPropertyOfType(parentType, text); + } + } + } + // In a variable, parameter or property declaration with a type annotation, // the contextual type of an initializer expression is the type of the variable, parameter or property. // Otherwise, in a parameter declaration of a contextually typed function expression, @@ -17020,32 +17183,13 @@ namespace ts { function getContextualTypeForInitializerExpression(node: Expression): Type | undefined { const declaration = node.parent; if (hasInitializer(declaration) && node === declaration.initializer) { - const typeNode = getEffectiveTypeAnnotationNode(declaration); - if (typeNode) { - return getTypeFromTypeNode(typeNode); + const result = getContextualTypeForVariableLikeDeclaration(declaration); + if (result) { + return result; } - if (declaration.kind === SyntaxKind.Parameter) { - const type = getContextuallyTypedParameterType(declaration); - if (type) { - return type; - } - } - if (isBindingPattern(declaration.name)) { + if (isBindingPattern(declaration.name)) { // This is less a contextual type and more an implied shape - in some cases, this may be undesirable return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false); } - if (isBindingPattern(declaration.parent)) { - const parentDeclaration = declaration.parent.parent; - const name = (declaration as BindingElement).propertyName || declaration.name; - if (parentDeclaration.kind !== SyntaxKind.BindingElement) { - const parentTypeNode = getEffectiveTypeAnnotationNode(parentDeclaration); - if (parentTypeNode && !isBindingPattern(name)) { - const text = getTextOfPropertyName(name); - if (text) { - return getTypeOfPropertyOfType(getTypeFromTypeNode(parentTypeNode), text); - } - } - } - } } return undefined; } @@ -17526,6 +17670,26 @@ namespace ts { } function getJsxPropsTypeForSignatureFromMember(sig: Signature, forcedLookupLocation: __String) { + if (sig.unionSignatures) { + // JSX Elements using the legacy `props`-field based lookup (eg, react class components) need to treat the `props` member as an input + // instead of an output position when resolving the signature. We need to go back to the input signatures of the composite signature, + // get the type of `props` on each return type individually, and then _intersect them_, rather than union them (as would normally occur + // for a union signature). It's an unfortunate quirk of looking in the output of the signature for the type we want to use for the input. + // The default behavior of `getTypeOfFirstParameterOfSignatureWithFallback` when no `props` member name is defined is much more sane. + const results: Type[] = []; + for (const signature of sig.unionSignatures) { + const instance = getReturnTypeOfSignature(signature); + if (isTypeAny(instance)) { + return instance; + } + const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation); + if (!propType) { + return; + } + results.push(propType); + } + return getIntersectionType(results); + } const instanceType = getReturnTypeOfSignature(sig); return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); } @@ -19116,8 +19280,8 @@ namespace ts { } } - function isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode, type: Type, property: Symbol): boolean { - return isValidPropertyAccessWithType(node, node.kind !== SyntaxKind.ImportType && node.expression.kind === SyntaxKind.SuperKeyword, property.escapedName, type) + function isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode | QualifiedName, type: Type, property: Symbol): boolean { + return isValidPropertyAccessWithType(node, node.kind === SyntaxKind.PropertyAccessExpression && node.expression.kind === SyntaxKind.SuperKeyword, property.escapedName, type) && (!(property.flags & SymbolFlags.Method) || isValidMethodAccess(property, type)); } function isValidMethodAccess(method: Symbol, actualThisType: Type): boolean { @@ -19711,7 +19875,7 @@ namespace ts { if (node.kind === SyntaxKind.CallExpression) { const callee = skipOuterExpressions(node.expression); if (callee.kind === SyntaxKind.PropertyAccessExpression || callee.kind === SyntaxKind.ElementAccessExpression) { - return (callee as PropertyAccessExpression | ElementAccessExpression).expression; + return (callee as AccessExpression).expression; } } } @@ -21743,7 +21907,7 @@ namespace ts { // Allow assignments to readonly properties within constructors of the same class declaration. if (symbol.flags & SymbolFlags.Property && (expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) && - (expr as PropertyAccessExpression | ElementAccessExpression).expression.kind === SyntaxKind.ThisKeyword) { + (expr as AccessExpression).expression.kind === SyntaxKind.ThisKeyword) { // Look for if this is the constructor for the class that `symbol` is a property of. const func = getContainingFunction(expr); if (!(func && func.kind === SyntaxKind.Constructor)) { @@ -21761,7 +21925,7 @@ namespace ts { function isReferenceThroughNamespaceImport(expr: Expression): boolean { if (expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) { - const node = skipParentheses((expr as PropertyAccessExpression | ElementAccessExpression).expression); + const node = skipParentheses((expr as AccessExpression).expression); if (node.kind === SyntaxKind.Identifier) { const symbol = getNodeLinks(node).resolvedSymbol!; if (symbol.flags & SymbolFlags.Alias) { @@ -22010,21 +22174,18 @@ namespace ts { function checkObjectLiteralDestructuringPropertyAssignment(objectLiteralType: Type, property: ObjectLiteralElementLike, allProperties?: NodeArray, rightIsThis = false) { if (property.kind === SyntaxKind.PropertyAssignment || property.kind === SyntaxKind.ShorthandPropertyAssignment) { const name = property.name; - if (name.kind === SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(name); - } - if (isComputedNonLiteralName(name)) { - return undefined; - } - - const type = getTypeOfObjectLiteralDestructuringProperty(objectLiteralType, name, property, rightIsThis); - if (type) { - // non-shorthand property assignments should always have initializers - return checkDestructuringAssignment(property.kind === SyntaxKind.ShorthandPropertyAssignment ? property : property.initializer, type); - } - else { - error(name, Diagnostics.Type_0_has_no_property_1_and_no_string_index_signature, typeToString(objectLiteralType), declarationNameToString(name)); + const text = getTextOfPropertyName(name); + if (text) { + const prop = getPropertyOfType(objectLiteralType, text); + if (prop) { + markPropertyAsReferenced(prop, property, rightIsThis); + checkPropertyAccessibility(property, /*isSuper*/ false, objectLiteralType, prop); + } } + const exprType = getLiteralTypeFromPropertyName(name); + const elementType = getIndexedAccessType(objectLiteralType, exprType, name); + const type = getFlowTypeOfDestructuring(property, elementType); + return checkDestructuringAssignment(property.kind === SyntaxKind.ShorthandPropertyAssignment ? property : property.initializer, type); } else if (property.kind === SyntaxKind.SpreadAssignment) { if (languageVersion < ScriptTarget.ESNext) { @@ -22045,31 +22206,11 @@ namespace ts { } } - function getTypeOfObjectLiteralDestructuringProperty(objectLiteralType: Type, name: PropertyName, property: PropertyAssignment | ShorthandPropertyAssignment, rightIsThis: boolean) { - if (isTypeAny(objectLiteralType)) { - return objectLiteralType; - } - - let type: Type | undefined; - const text = getTextOfPropertyName(name); - if (text) { // TODO: GH#26379 - const prop = getPropertyOfType(objectLiteralType, text); - if (prop) { - markPropertyAsReferenced(prop, property, rightIsThis); - checkPropertyAccessibility(property, /*isSuper*/ false, objectLiteralType, prop); - type = getTypeOfSymbol(prop); - } - type = type || (isNumericLiteralName(text) ? getIndexTypeOfType(objectLiteralType, IndexKind.Number) : undefined); - } - return type || getIndexTypeOfType(objectLiteralType, IndexKind.String); - } - function checkArrayLiteralAssignment(node: ArrayLiteralExpression, sourceType: Type, checkMode?: CheckMode): Type { const elements = node.elements; if (languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { checkExternalEmitHelpers(node, ExternalEmitHelpers.Read); } - // This elementType will be used if the specific property corresponding to this index is not // present (aka the tuple element property). This call also checks that the parentType is in // fact an iterable or array (depending on target language). @@ -22086,39 +22227,30 @@ namespace ts { const element = elements[elementIndex]; if (element.kind !== SyntaxKind.OmittedExpression) { if (element.kind !== SyntaxKind.SpreadElement) { - const propName = "" + elementIndex as __String; - const type = isTypeAny(sourceType) ? sourceType : - everyType(sourceType, isTupleLikeType) ? getTupleElementType(sourceType, elementIndex) : - elementType; - if (type) { + const indexType = getLiteralType(elementIndex); + if (isArrayLikeType(sourceType)) { + // We create a synthetic expression so that getIndexedAccessType doesn't get confused + // when the element is a SyntaxKind.ElementAccessExpression. + const elementType = getIndexedAccessType(sourceType, indexType, createSyntheticExpression(element, indexType)); + const type = getFlowTypeOfDestructuring(element, elementType); return checkDestructuringAssignment(element, type, checkMode); } - // We still need to check element expression here because we may need to set appropriate flag on the expression - // such as NodeCheckFlags.LexicalThis on "this"expression. - checkExpression(element); - if (isTupleType(sourceType)) { - error(element, Diagnostics.Tuple_type_0_with_length_1_cannot_be_assigned_to_tuple_with_length_2, typeToString(sourceType), getTypeReferenceArity(sourceType), elements.length); - } - else { - error(element, Diagnostics.Type_0_has_no_property_1, typeToString(sourceType), propName as string); - } + return checkDestructuringAssignment(element, elementType, checkMode); + } + if (elementIndex < elements.length - 1) { + error(element, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); } else { - if (elementIndex < elements.length - 1) { - error(element, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); + const restExpression = (element).expression; + if (restExpression.kind === SyntaxKind.BinaryExpression && (restExpression).operatorToken.kind === SyntaxKind.EqualsToken) { + error((restExpression).operatorToken, Diagnostics.A_rest_element_cannot_have_an_initializer); } else { - const restExpression = (element).expression; - if (restExpression.kind === SyntaxKind.BinaryExpression && (restExpression).operatorToken.kind === SyntaxKind.EqualsToken) { - error((restExpression).operatorToken, Diagnostics.A_rest_element_cannot_have_an_initializer); - } - else { - checkGrammarForDisallowedTrailingComma(node.elements, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - const type = everyType(sourceType, isTupleType) ? - mapType(sourceType, t => sliceTupleType(t, elementIndex)) : - createArrayType(elementType); - return checkDestructuringAssignment(restExpression, type, checkMode); - } + checkGrammarForDisallowedTrailingComma(node.elements, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + const type = everyType(sourceType, isTupleType) ? + mapType(sourceType, t => sliceTupleType(t, elementIndex)) : + createArrayType(elementType); + return checkDestructuringAssignment(restExpression, type, checkMode); } } } @@ -22695,7 +22827,8 @@ namespace ts { const type = getTypeOfExpression(initializer, /*cache*/ true); const widened = getCombinedNodeFlags(declaration) & NodeFlags.Const || isDeclarationReadonly(declaration) || - isTypeAssertion(initializer) ? type : getWidenedLiteralType(type); + isTypeAssertion(initializer) || + isLiteralOfContextualType(type, getContextualType(initializer)) ? type : getWidenedLiteralType(type); if (isInJSFile(declaration)) { if (widened.flags & TypeFlags.Nullable) { reportImplicitAny(declaration, anyType); @@ -26350,6 +26483,7 @@ namespace ts { if (produceDiagnostics) { if (node.default) { seenDefault = true; + checkTypeParametersNotReferenced(node.default, typeParameterDeclarations, i); } else if (seenDefault) { error(node, Diagnostics.Required_type_parameters_may_not_follow_optional_type_parameters); @@ -26364,6 +26498,24 @@ namespace ts { } } + /** Check that type parameter defaults only reference previously declared type parameters */ + function checkTypeParametersNotReferenced(root: TypeNode, typeParameters: ReadonlyArray, index: number) { + visit(root); + function visit(node: Node) { + if (node.kind === SyntaxKind.TypeReference) { + const type = getTypeFromTypeReference(node); + if (type.flags & TypeFlags.TypeParameter) { + for (let i = index; i < typeParameters.length; i++) { + if (type.symbol === getSymbolOfNode(typeParameters[i])) { + error(node, Diagnostics.Type_parameter_defaults_can_only_reference_previously_declared_type_parameters); + } + } + } + } + forEachChild(node, visit); + } + } + /** Check that type parameter lists are identical across multiple declarations */ function checkTypeParameterListsIdentical(symbol: Symbol) { if (symbol.declarations.length === 1) { @@ -26896,7 +27048,7 @@ namespace ts { return 0; } else if (isConstEnum) { - error(initializer, Diagnostics.In_const_enum_declarations_member_initializer_must_be_constant_expression); + error(initializer, Diagnostics.const_enum_member_initializers_can_only_contain_literal_values_and_other_computed_enum_values); } else if (member.parent.flags & NodeFlags.Ambient) { error(initializer, Diagnostics.In_ambient_enum_declarations_member_initializer_must_be_constant_expression); @@ -26957,7 +27109,7 @@ namespace ts { return nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), identifier.escapedText); case SyntaxKind.ElementAccessExpression: case SyntaxKind.PropertyAccessExpression: - const ex = expr; + const ex = expr; if (isConstantMemberAccess(ex)) { const type = getTypeOfExpression(ex.expression); if (type.symbol && type.symbol.flags & SymbolFlags.Enum) { @@ -28931,7 +29083,7 @@ namespace ts { return getNodeLinks(node).enumMemberValue; } - function canHaveConstantValue(node: Node): node is EnumMember | PropertyAccessExpression | ElementAccessExpression { + function canHaveConstantValue(node: Node): node is EnumMember | AccessExpression { switch (node.kind) { case SyntaxKind.EnumMember: case SyntaxKind.PropertyAccessExpression: @@ -28941,7 +29093,7 @@ namespace ts { return false; } - function getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined { + function getConstantValue(node: EnumMember | AccessExpression): string | number | undefined { if (node.kind === SyntaxKind.EnumMember) { return getEnumMemberValue(node); } @@ -30930,8 +31082,4 @@ namespace ts { export const LibraryManagedAttributes = "LibraryManagedAttributes" as __String; // tslint:enable variable-name } - - function typeIsLiteralType(type: Type): type is LiteralType { - return !!(type.flags & TypeFlags.Literal); - } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 322355117ec..cb9e385b366 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -112,13 +112,13 @@ namespace ts { } // The global Map object. This may not be available, so we must test for it. - declare const Map: { new (): Map } | undefined; + declare const Map: (new () => Map) | undefined; // Internet Explorer's Map doesn't support iteration, so don't use it. // tslint:disable-next-line no-in-operator variable-name export const MapCtr = typeof Map !== "undefined" && "entries" in Map.prototype ? Map : shimMap(); // Keep the class inside a function so it doesn't get compiled if it's not used. - function shimMap(): { new (): Map } { + function shimMap(): new () => Map { class MapIterator { private data: MapLike; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2cbf5784d07..57f4c4c5611 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1015,6 +1015,14 @@ "category": "Error", "code": 1351 }, + "A bigint literal cannot use exponential notation.": { + "category": "Error", + "code": 1352 + }, + "A bigint literal must be an integer.": { + "category": "Error", + "code": 1353 + }, "Duplicate identifier '{0}'.": { "category": "Error", @@ -1636,14 +1644,6 @@ "category": "Error", "code": 2458 }, - "Type '{0}' has no property '{1}' and no string index signature.": { - "category": "Error", - "code": 2459 - }, - "Type '{0}' has no property '{1}'.": { - "category": "Error", - "code": 2460 - }, "Type '{0}' is not an array type.": { "category": "Error", "code": 2461 @@ -1696,7 +1696,7 @@ "category": "Error", "code": 2473 }, - "In 'const' enum declarations member initializer must be constant expression.": { + "const enum member initializers can only contain literal values and other computed enum values.": { "category": "Error", "code": 2474 }, @@ -1760,7 +1760,7 @@ "category": "Error", "code": 2492 }, - "Tuple type '{0}' with length '{1}' cannot be assigned to tuple with length '{2}'.": { + "Tuple type '{0}' of length '{1}' has no element at index '{2}'.": { "category": "Error", "code": 2493 }, @@ -2537,6 +2537,10 @@ "category": "Error", "code": 2743 }, + "Type parameter defaults can only reference previously declared type parameters.": { + "category": "Error", + "code": 2744 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 15b6540900f..9c09ed79a77 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1651,11 +1651,17 @@ namespace ts { function emitPropertyAccessExpression(node: PropertyAccessExpression) { let indentBeforeDot = false; let indentAfterDot = false; + const dotRangeFirstCommentStart = skipTrivia( + currentSourceFile!.text, + node.expression.end, + /*stopAfterLineBreak*/ false, + /*stopAtComments*/ true + ); + const dotRangeStart = skipTrivia(currentSourceFile!.text, dotRangeFirstCommentStart); + const dotRangeEnd = dotRangeStart + 1; if (!(getEmitFlags(node) & EmitFlags.NoIndentation)) { - const dotRangeStart = node.expression.end; - const dotRangeEnd = skipTrivia(currentSourceFile!.text, node.expression.end) + 1; const dotToken = createToken(SyntaxKind.DotToken); - dotToken.pos = dotRangeStart; + dotToken.pos = node.expression.end; dotToken.end = dotRangeEnd; indentBeforeDot = needsIndentation(node, node.expression, dotToken); indentAfterDot = needsIndentation(node, dotToken, node.name); @@ -1664,7 +1670,8 @@ namespace ts { emitExpression(node.expression); increaseIndentIf(indentBeforeDot, /*writeSpaceIfNotIndenting*/ false); - const shouldEmitDotDot = !indentBeforeDot && needsDotDotForPropertyAccess(node.expression); + const dotHasCommentTrivia = dotRangeFirstCommentStart !== dotRangeStart; + const shouldEmitDotDot = !indentBeforeDot && needsDotDotForPropertyAccess(node.expression, dotHasCommentTrivia); if (shouldEmitDotDot) { writePunctuation("."); } @@ -1677,13 +1684,15 @@ namespace ts { // 1..toString is a valid property access, emit a dot after the literal // Also emit a dot if expression is a integer const enum value - it will appear in generated code as numeric literal - function needsDotDotForPropertyAccess(expression: Expression) { + function needsDotDotForPropertyAccess(expression: Expression, dotHasTrivia: boolean) { expression = skipPartiallyEmittedExpressions(expression); if (isNumericLiteral(expression)) { // check if numeric literal is a decimal literal that was originally written with a dot const text = getLiteralTextOfNode(expression, /*neverAsciiEscape*/ true); - return !expression.numericLiteralFlags - && !stringContains(text, tokenToString(SyntaxKind.DotToken)!); + // If he number will be printed verbatim and it doesn't already contain a dot, add one + // if the expression doesn't have any comments that will be emitted. + return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!) && + (!dotHasTrivia || printerOptions.removeComments); } else if (isPropertyAccessExpression(expression) || isElementAccessExpression(expression)) { // check if constant enum value is integer diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 1789b161e39..080b17b8cbe 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -787,7 +787,13 @@ namespace ts { // Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it. let redirectTargetsMap = createMultiMap(); - const filesByName = createMap(); + /** + * map with + * - SourceFile if present + * - false if sourceFile missing for source of project reference redirect + * - undefined otherwise + */ + const filesByName = createMap(); let missingFilePaths: ReadonlyArray | undefined; // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing @@ -854,7 +860,7 @@ namespace ts { } } - missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); + missingFilePaths = arrayFrom(mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as Path : undefined)); files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles); processingDefaultLibFiles = undefined; processingOtherFiles = undefined; @@ -1567,7 +1573,7 @@ namespace ts { } function getSourceFileByPath(path: Path): SourceFile | undefined { - return filesByName.get(path); + return filesByName.get(path) || undefined; } function getDiagnosticsHelper( @@ -2104,7 +2110,7 @@ namespace ts { /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ function getSourceFileFromReference(referencingFile: SourceFile, ref: FileReference): SourceFile | undefined { - return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName))); + return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName)) || undefined); } function getSourceFileFromReferenceWorker( @@ -2235,7 +2241,7 @@ namespace ts { } } - return file; + return file || undefined; } let redirectedPath: Path | undefined; @@ -2327,9 +2333,12 @@ namespace ts { } function addFileToFilesByName(file: SourceFile | undefined, path: Path, redirectedPath: Path | undefined) { - filesByName.set(path, file); if (redirectedPath) { filesByName.set(redirectedPath, file); + filesByName.set(path, file || false); + } + else { + filesByName.set(path, file); } } diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index a9438110593..d59be975865 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -337,13 +337,14 @@ namespace ts { return result; } - export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number { - return computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text); - } - + export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number; /* @internal */ - export function getPositionOfLineAndCharacterWithEdits(sourceFile: SourceFileLike, line: number, character: number): number { - return computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, /*allowEdits*/ true); + // tslint:disable-next-line:unified-signatures + export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number; + export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number { + return sourceFile.getPositionOfLineAndCharacter ? + sourceFile.getPositionOfLineAndCharacter(line, character, allowEdits) : + computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, allowEdits); } /* @internal */ @@ -976,7 +977,7 @@ namespace ts { } if (decimalFragment !== undefined || tokenFlags & TokenFlags.Scientific) { - checkForIdentifierStartAfterNumericLiteral(); + checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & TokenFlags.Scientific)); return { type: SyntaxKind.NumericLiteral, value: "" + +result // if value is not an integer, it can be safely coerced to a number @@ -985,20 +986,31 @@ namespace ts { else { tokenValue = result; const type = checkBigIntSuffix(); // if value is an integer, check whether it is a bigint - checkForIdentifierStartAfterNumericLiteral(); + checkForIdentifierStartAfterNumericLiteral(start); return { type, value: tokenValue }; } } - function checkForIdentifierStartAfterNumericLiteral() { + function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) { if (!isIdentifierStart(text.charCodeAt(pos), languageVersion)) { return; } const identifierStart = pos; const { length } = scanIdentifierParts(); - error(Diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, identifierStart, length); - pos = identifierStart; + + if (length === 1 && text[identifierStart] === "n") { + if (isScientific) { + error(Diagnostics.A_bigint_literal_cannot_use_exponential_notation, numericStart, identifierStart - numericStart + 1); + } + else { + error(Diagnostics.A_bigint_literal_must_be_an_integer, numericStart, identifierStart - numericStart + 1); + } + } + else { + error(Diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, identifierStart, length); + pos = identifierStart; + } } function scanOctalDigits(): number { diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 9b74fb22d58..c2597d688d8 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -266,14 +266,24 @@ namespace ts { const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\s*$/; const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; + export interface LineInfo { + getLineCount(): number; + getLineText(line: number): string; + } + + export function getLineInfo(text: string, lineStarts: ReadonlyArray): LineInfo { + return { + getLineCount: () => lineStarts.length, + getLineText: line => text.substring(lineStarts[line], lineStarts[line + 1]) + }; + } + /** * Tries to find the sourceMappingURL comment at the end of a file. - * @param text The source text of the file. - * @param lineStarts The line starts of the file. */ - export function tryGetSourceMappingURL(text: string, lineStarts: ReadonlyArray = computeLineStarts(text)) { - for (let index = lineStarts.length - 1; index >= 0; index--) { - const line = text.substring(lineStarts[index], lineStarts[index + 1]); + export function tryGetSourceMappingURL(lineInfo: LineInfo) { + for (let index = lineInfo.getLineCount() - 1; index >= 0; index--) { + const line = lineInfo.getLineText(index); const comment = sourceMapCommentRegExp.exec(line); if (comment) { return comment[1]; @@ -573,7 +583,10 @@ namespace ts { } function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) { - return compareValues(left.sourceIndex, right.sourceIndex); + // Compares sourcePosition without comparing sourceIndex + // since the mappings are grouped by sourceIndex + Debug.assert(left.sourceIndex === right.sourceIndex); + return compareValues(left.sourcePosition, right.sourcePosition); } function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) { @@ -592,11 +605,9 @@ namespace ts { const mapDirectory = getDirectoryPath(mapPath); const sourceRoot = map.sourceRoot ? getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory; const generatedAbsoluteFilePath = getNormalizedAbsolutePath(map.file, mapDirectory); - const generatedCanonicalFilePath = host.getCanonicalFileName(generatedAbsoluteFilePath) as Path; - const generatedFile = host.getSourceFileLike(generatedCanonicalFilePath); + const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath); const sourceFileAbsolutePaths = map.sources.map(source => getNormalizedAbsolutePath(source, sourceRoot)); - const sourceFileCanonicalPaths = sourceFileAbsolutePaths.map(source => host.getCanonicalFileName(source) as Path); - const sourceToSourceIndexMap = createMapFromEntries(sourceFileCanonicalPaths.map((source, i) => [source, i] as [string, number])); + const sourceToSourceIndexMap = createMapFromEntries(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i] as [string, number])); let decodedMappings: ReadonlyArray | undefined; let generatedMappings: SortedReadonlyArray | undefined; let sourceMappings: ReadonlyArray> | undefined; @@ -608,16 +619,15 @@ namespace ts { function processMapping(mapping: Mapping): MappedPosition { const generatedPosition = generatedFile !== undefined - ? getPositionOfLineAndCharacterWithEdits(generatedFile, mapping.generatedLine, mapping.generatedCharacter) + ? getPositionOfLineAndCharacter(generatedFile, mapping.generatedLine, mapping.generatedCharacter, /*allowEdits*/ true) : -1; let source: string | undefined; let sourcePosition: number | undefined; if (isSourceMapping(mapping)) { - const sourceFilePath = sourceFileCanonicalPaths[mapping.sourceIndex]; - const sourceFile = host.getSourceFileLike(sourceFilePath); + const sourceFile = host.getSourceFileLike(sourceFileAbsolutePaths[mapping.sourceIndex]); source = map.sources[mapping.sourceIndex]; sourcePosition = sourceFile !== undefined - ? getPositionOfLineAndCharacterWithEdits(sourceFile, mapping.sourceLine, mapping.sourceCharacter) + ? getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*allowEdits*/ true) : -1; } return { diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index 973b9db618e..b54cf5bb858 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -1100,7 +1100,8 @@ namespace ts { if (extendsClause && !isEntityNameExpression(extendsClause.expression) && extendsClause.expression.kind !== SyntaxKind.NullKeyword) { // We must add a temporary declaration for the extends clause expression - const newId = createOptimisticUniqueName(`${unescapeLeadingUnderscores(input.name!.escapedText)}_base`); // TODO: GH#18217 + const oldId = input.name ? unescapeLeadingUnderscores(input.name.escapedText) : "default"; + const newId = createOptimisticUniqueName(`${oldId}_base`); getSymbolAccessibilityDiagnostic = () => ({ diagnosticMessage: Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1, errorNode: extendsClause, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 495465f7032..5c42d3eb510 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1745,6 +1745,9 @@ namespace ts { export type EntityNameExpression = Identifier | PropertyAccessEntityNameExpression; export type EntityNameOrEntityNameExpression = EntityName | EntityNameExpression; + /* @internal */ + export type AccessExpression = PropertyAccessExpression | ElementAccessExpression; + export interface PropertyAccessExpression extends MemberExpression, NamedDeclaration { kind: SyntaxKind.PropertyAccessExpression; expression: LeftHandSideExpression; @@ -2614,6 +2617,8 @@ namespace ts { export interface SourceFileLike { readonly text: string; lineMap?: ReadonlyArray; + /* @internal */ + getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number; } @@ -3103,7 +3108,7 @@ namespace ts { getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined; isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: string): boolean; /** Exclude accesses to private properties or methods with a `this` parameter that `type` doesn't satisfy. */ - /* @internal */ isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode, type: Type, property: Symbol): boolean; + /* @internal */ isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode | QualifiedName, type: Type, property: Symbol): boolean; /** Follow all aliases to get the original symbol. */ getAliasedSymbol(symbol: Symbol): Symbol; /** Follow a *single* alias to get the immediately aliased symbol. */ @@ -3672,15 +3677,17 @@ namespace ts { Readonly = 1 << 3, // Readonly transient symbol Partial = 1 << 4, // Synthetic property present in some but not all constituents HasNonUniformType = 1 << 5, // Synthetic property with non-uniform type in constituents - ContainsPublic = 1 << 6, // Synthetic property with public constituent(s) - ContainsProtected = 1 << 7, // Synthetic property with protected constituent(s) - ContainsPrivate = 1 << 8, // Synthetic property with private constituent(s) - ContainsStatic = 1 << 9, // Synthetic property with static constituent(s) - Late = 1 << 10, // Late-bound symbol for a computed property with a dynamic name - ReverseMapped = 1 << 11, // Property of reverse-inferred homomorphic mapped type - OptionalParameter = 1 << 12, // Optional parameter - RestParameter = 1 << 13, // Rest parameter - Synthetic = SyntheticProperty | SyntheticMethod + HasLiteralType = 1 << 6, // Synthetic property with at least one literal type in constituents + ContainsPublic = 1 << 7, // Synthetic property with public constituent(s) + ContainsProtected = 1 << 8, // Synthetic property with protected constituent(s) + ContainsPrivate = 1 << 9, // Synthetic property with private constituent(s) + ContainsStatic = 1 << 10, // Synthetic property with static constituent(s) + Late = 1 << 11, // Late-bound symbol for a computed property with a dynamic name + ReverseMapped = 1 << 12, // Property of reverse-inferred homomorphic mapped type + OptionalParameter = 1 << 13, // Optional parameter + RestParameter = 1 << 14, // Rest parameter + Synthetic = SyntheticProperty | SyntheticMethod, + Discriminant = HasNonUniformType | HasLiteralType } /* @internal */ @@ -5528,9 +5535,9 @@ namespace ts { /* @internal */ export interface DocumentPositionMapperHost { - getSourceFileLike(path: Path): SourceFileLike | undefined; + getSourceFileLike(fileName: string): SourceFileLike | undefined; getCanonicalFileName(path: string): string; - log?(text: string): void; + log(text: string): void; } /** diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 966149f2a6c..9c6b27de13c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2121,7 +2121,7 @@ namespace ts { : undefined; } - function getSingleInitializerOfVariableStatementOrPropertyDeclaration(node: Node): Expression | undefined { + export function getSingleInitializerOfVariableStatementOrPropertyDeclaration(node: Node): Expression | undefined { switch (node.kind) { case SyntaxKind.VariableStatement: const v = getSingleVariableOfVariableStatement(node); @@ -4590,6 +4590,10 @@ namespace ts { || kind === SyntaxKind.JSDocFunctionType || kind === SyntaxKind.JSDocVariadicType; } + + export function isAccessExpression(node: Node): node is AccessExpression { + return node.kind === SyntaxKind.PropertyAccessExpression || node.kind === SyntaxKind.ElementAccessExpression; + } } namespace ts { @@ -6925,7 +6929,7 @@ namespace ts { export interface ObjectAllocator { getNodeConstructor(): new (kind: SyntaxKind, pos?: number, end?: number) => Node; - getTokenConstructor(): { new (kind: TKind, pos?: number, end?: number): Token }; + getTokenConstructor(): new (kind: TKind, pos?: number, end?: number) => Token; getIdentifierConstructor(): new (kind: SyntaxKind.Identifier, pos?: number, end?: number) => Identifier; getSourceFileConstructor(): new (kind: SyntaxKind.SourceFile, pos?: number, end?: number) => SourceFile; getSymbolConstructor(): new (flags: SymbolFlags, name: __String) => Symbol; diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index b02aff523d1..db1c326a504 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -600,7 +600,7 @@ namespace FourSlash { throw new Error("Expected exactly one output from emit of " + this.activeFile.fileName); } - const evaluation = new Function(`${emit.outputFiles[0].text};\r\nreturn (${expr});`)(); + const evaluation = new Function(`${emit.outputFiles[0].text};\r\nreturn (${expr});`)(); // tslint:disable-line:function-constructor if (evaluation !== value) { this.raiseError(`Expected evaluation of expression "${expr}" to equal "${value}", but got "${evaluation}"`); } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index fc275529258..13ef2adbf90 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -28,12 +28,10 @@ var assert: typeof _chai.assert = _chai.assert; }; } -var global: NodeJS.Global = Function("return this").call(undefined); +var global: NodeJS.Global = Function("return this").call(undefined); // tslint:disable-line:function-constructor declare var window: {}; -declare var XMLHttpRequest: { - new(): XMLHttpRequest; -}; +declare var XMLHttpRequest: new() => XMLHttpRequest; interface XMLHttpRequest { readonly readyState: number; readonly responseText: string; diff --git a/src/harness/sourceMapRecorder.ts b/src/harness/sourceMapRecorder.ts index 92c3e2d4a94..d16aca21d66 100644 --- a/src/harness/sourceMapRecorder.ts +++ b/src/harness/sourceMapRecorder.ts @@ -69,7 +69,7 @@ namespace Harness.SourceMapRecorder { SourceMapDecoder.initializeSourceMapDecoding(sourceMapData); sourceMapRecorder.WriteLine("==================================================================="); sourceMapRecorder.WriteLine("JsFile: " + sourceMapData.sourceMap.file); - sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(jsFile.text, jsLineMap)); + sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(ts.getLineInfo(jsFile.text, jsLineMap))); sourceMapRecorder.WriteLine("sourceRoot: " + sourceMapData.sourceMap.sourceRoot); sourceMapRecorder.WriteLine("sources: " + sourceMapData.sourceMap.sources); if (sourceMapData.sourceMap.sourcesContent) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 90b516dadfc..a823fe6d586 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -342,6 +342,7 @@ namespace ts.server { FailedLookupLocation = "Directory of Failed lookup locations in module resolution", TypeRoots = "Type root directory", NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them", + MissingSourceMapFile = "Missing source map file" } const enum ConfigFileWatcherStatus { @@ -437,7 +438,8 @@ namespace ts.server { /** * Container of all known scripts */ - private readonly filenameToScriptInfo = createMap(); + /*@internal*/ + readonly filenameToScriptInfo = createMap(); private readonly scriptInfoInNodeModulesWatchers = createMap (); /** * Contains all the deleted script info's version information so that @@ -944,10 +946,42 @@ namespace ts.server { // this file and set of inferred projects info.delayReloadNonMixedContentFile(); this.delayUpdateProjectGraphs(info.containingProjects); + this.handleSourceMapProjects(info); } } } + private handleSourceMapProjects(info: ScriptInfo) { + // Change in d.ts, update source projects as well + if (info.sourceMapFilePath) { + if (isString(info.sourceMapFilePath)) { + const sourceMapFileInfo = this.getScriptInfoForPath(info.sourceMapFilePath); + this.delayUpdateSourceInfoProjects(sourceMapFileInfo && sourceMapFileInfo.sourceInfos); + } + else { + this.delayUpdateSourceInfoProjects(info.sourceMapFilePath.sourceInfos); + } + } + // Change in mapInfo, update declarationProjects and source projects + this.delayUpdateSourceInfoProjects(info.sourceInfos); + if (info.declarationInfoPath) { + this.delayUpdateProjectsOfScriptInfoPath(info.declarationInfoPath); + } + } + + private delayUpdateSourceInfoProjects(sourceInfos: Map | undefined) { + if (sourceInfos) { + sourceInfos.forEach((_value, path) => this.delayUpdateProjectsOfScriptInfoPath(path as Path)); + } + } + + private delayUpdateProjectsOfScriptInfoPath(path: Path) { + const info = this.getScriptInfoForPath(path); + if (info) { + this.delayUpdateProjectGraphs(info.containingProjects); + } + } + private handleDeletedFile(info: ScriptInfo) { this.stopWatchingScriptInfo(info); @@ -961,6 +995,15 @@ namespace ts.server { // update projects to make sure that set of referenced files is correct this.delayUpdateProjectGraphs(containingProjects); + this.handleSourceMapProjects(info); + info.closeSourceMapFileWatcher(); + // need to recalculate source map from declaration file + if (info.declarationInfoPath) { + const declarationInfo = this.getScriptInfoForPath(info.declarationInfoPath); + if (declarationInfo) { + declarationInfo.sourceMapFilePath = undefined; + } + } } } @@ -1234,7 +1277,8 @@ namespace ts.server { private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) { const configFileExistenceInfo = this.getConfigFileExistenceInfo(project); if (configFileExistenceInfo) { - Debug.assert(configFileExistenceInfo.exists); + // The existance might not be set if the file watcher is not invoked by the time config project is created by external project + configFileExistenceInfo.exists = true; // close existing watcher if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { const configFileName = project.getConfigFilePath(); @@ -2195,6 +2239,150 @@ namespace ts.server { return this.filenameToScriptInfo.get(fileName); } + /*@internal*/ + getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { + // Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host + const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host); + if (!declarationInfo) return undefined; + + // Try to get from cache + declarationInfo.getSnapshot(); // Ensure synchronized + if (isString(declarationInfo.sourceMapFilePath)) { + // Ensure mapper is synchronized + const sourceMapFileInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath); + if (sourceMapFileInfo) { + sourceMapFileInfo.getSnapshot(); + if (sourceMapFileInfo.documentPositionMapper !== undefined) { + sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos); + return sourceMapFileInfo.documentPositionMapper ? sourceMapFileInfo.documentPositionMapper : undefined; + } + } + declarationInfo.sourceMapFilePath = undefined; + } + else if (declarationInfo.sourceMapFilePath) { + declarationInfo.sourceMapFilePath.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, declarationInfo.sourceMapFilePath.sourceInfos); + return undefined; + } + else if (declarationInfo.sourceMapFilePath !== undefined) { + // Doesnt have sourceMap + return undefined; + } + + // Create the mapper + let sourceMapFileInfo: ScriptInfo | undefined; + let mapFileNameFromDeclarationInfo: string | undefined; + + let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => { + const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host); + if (!mapInfo) { + mapFileNameFromDeclarationInfo = mapFileNameFromDts; + return undefined; + } + sourceMapFileInfo = mapInfo; + const snap = mapInfo.getSnapshot(); + if (mapInfo.documentPositionMapper !== undefined) return mapInfo.documentPositionMapper; + return snap.getText(0, snap.getLength()); + }; + const projectName = project.projectName; + const documentPositionMapper = getDocumentPositionMapper( + { getCanonicalFileName: this.toCanonicalFileName, log: s => this.logger.info(s), getSourceFileLike: f => this.getSourceFileLike(f, projectName, declarationInfo) }, + declarationInfo.fileName, + declarationInfo.getLineInfo(), + readMapFile + ); + readMapFile = undefined; // Remove ref to project + if (sourceMapFileInfo) { + declarationInfo.sourceMapFilePath = sourceMapFileInfo.path; + sourceMapFileInfo.declarationInfoPath = declarationInfo.path; + sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false; + sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos); + } + else if (mapFileNameFromDeclarationInfo) { + declarationInfo.sourceMapFilePath = { + watcher: this.addMissingSourceMapFile( + project.currentDirectory === this.currentDirectory ? + mapFileNameFromDeclarationInfo : + getNormalizedAbsolutePath(mapFileNameFromDeclarationInfo, project.currentDirectory), + declarationInfo.path + ), + sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project) + }; + } + else { + declarationInfo.sourceMapFilePath = false; + } + return documentPositionMapper; + } + + private addSourceInfoToSourceMap(sourceFileName: string | undefined, project: Project, sourceInfos?: Map) { + if (sourceFileName) { + // Attach as source + const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(sourceFileName, project.currentDirectory, project.directoryStructureHost)!; + (sourceInfos || (sourceInfos = createMap())).set(sourceInfo.path, true); + } + return sourceInfos; + } + + private addMissingSourceMapFile(mapFileName: string, declarationInfoPath: Path) { + const fileWatcher = this.watchFactory.watchFile( + this.host, + mapFileName, + () => { + const declarationInfo = this.getScriptInfoForPath(declarationInfoPath); + if (declarationInfo && declarationInfo.sourceMapFilePath && !isString(declarationInfo.sourceMapFilePath)) { + // Update declaration and source projects + this.delayUpdateProjectGraphs(declarationInfo.containingProjects); + this.delayUpdateSourceInfoProjects(declarationInfo.sourceMapFilePath.sourceInfos); + declarationInfo.closeSourceMapFileWatcher(); + } + }, + PollingInterval.High, + WatchType.MissingSourceMapFile, + ); + return fileWatcher; + } + + /*@internal*/ + getSourceFileLike(fileName: string, projectNameOrProject: string | Project, declarationInfo?: ScriptInfo) { + const project = (projectNameOrProject as Project).projectName ? projectNameOrProject as Project : this.findProject(projectNameOrProject as string); + if (project) { + const path = project.toPath(fileName); + const sourceFile = project.getSourceFile(path); + if (sourceFile && sourceFile.resolvedPath === path) return sourceFile; + } + + // Need to look for other files. + const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host); + if (!info) return undefined; + + // Attach as source + if (declarationInfo && isString(declarationInfo.sourceMapFilePath) && info !== declarationInfo) { + const sourceMapInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath); + if (sourceMapInfo) { + (sourceMapInfo.sourceInfos || (sourceMapInfo.sourceInfos = createMap())).set(info.path, true); + } + } + + // Key doesnt matter since its only for text and lines + if (info.cacheSourceFile) return info.cacheSourceFile.sourceFile; + + // Create sourceFileLike + if (!info.sourceFileLike) { + info.sourceFileLike = { + get text() { + Debug.fail("shouldnt need text"); + return ""; + }, + getLineAndCharacterOfPosition: pos => { + const lineOffset = info.positionToLineOffset(pos); + return { line: lineOffset.line - 1, character: lineOffset.offset - 1 }; + }, + getPositionOfLineAndCharacter: (line, character, allowEdits) => info.lineOffsetToPosition(line + 1, character + 1, allowEdits) + }; + } + return info.sourceFileLike; + } + setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); @@ -2416,7 +2604,7 @@ namespace ts.server { /** @internal */ fileExists(fileName: NormalizedPath): boolean { - return this.filenameToScriptInfo.has(fileName) || this.host.fileExists(fileName); + return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName); } private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined { @@ -2490,13 +2678,7 @@ namespace ts.server { // when some file/s were closed which resulted in project removal. // It was then postponed to cleanup these script infos so that they can be reused if // the file from that old project is reopened because of opening file from here. - this.filenameToScriptInfo.forEach(info => { - if (!info.isScriptOpen() && info.isOrphan()) { - // if there are not projects that include this script info - delete it - this.stopWatchingScriptInfo(info); - this.deleteScriptInfo(info); - } - }); + this.removeOrphanScriptInfos(); this.printProjects(); @@ -2539,6 +2721,57 @@ namespace ts.server { } } + private removeOrphanScriptInfos() { + const toRemoveScriptInfos = cloneMap(this.filenameToScriptInfo); + this.filenameToScriptInfo.forEach(info => { + // If script info is open or orphan, retain it and its dependencies + if (!info.isScriptOpen() && info.isOrphan()) { + // Otherwise if there is any source info that is alive, this alive too + if (!info.sourceMapFilePath) return; + let sourceInfos: Map | undefined; + if (isString(info.sourceMapFilePath)) { + const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath); + sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos; + } + else { + sourceInfos = info.sourceMapFilePath.sourceInfos; + } + if (!sourceInfos) return; + if (!forEachKey(sourceInfos, path => { + const info = this.getScriptInfoForPath(path as Path); + return !!info && (info.isScriptOpen() || !info.isOrphan()); + })) { + return; + } + } + + // Retain this script info + toRemoveScriptInfos.delete(info.path); + if (info.sourceMapFilePath) { + let sourceInfos: Map | undefined; + if (isString(info.sourceMapFilePath)) { + // And map file info and source infos + toRemoveScriptInfos.delete(info.sourceMapFilePath); + const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath); + sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos; + } + else { + sourceInfos = info.sourceMapFilePath.sourceInfos; + } + if (sourceInfos) { + sourceInfos.forEach((_value, path) => toRemoveScriptInfos.delete(path)); + } + } + }); + + toRemoveScriptInfos.forEach(info => { + // if there are not projects that include this script info - delete it + this.stopWatchingScriptInfo(info); + this.deleteScriptInfo(info); + info.closeSourceMapFileWatcher(); + }); + } + private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { if (this.syntaxOnly || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { return; diff --git a/src/server/project.ts b/src/server/project.ts index fa39aa4abe9..e3069e1d183 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -503,6 +503,16 @@ namespace ts.server { return this.getLanguageService().getSourceMapper(); } + /*@internal*/ + getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { + return this.projectService.getDocumentPositionMapper(this, generatedFileName, sourceFileName); + } + + /*@internal*/ + getSourceFileLike(fileName: string) { + return this.projectService.getSourceFileLike(fileName, this); + } + private shouldEmitFile(scriptInfo: ScriptInfo) { return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); } @@ -749,7 +759,10 @@ namespace ts.server { } containsScriptInfo(info: ScriptInfo): boolean { - return this.isRoot(info) || (!!this.program && this.program.getSourceFileByPath(info.path) !== undefined); + if (this.isRoot(info)) return true; + if (!this.program) return false; + const file = this.program.getSourceFileByPath(info.path); + return !!file && file.resolvedPath === info.path; } containsFile(filename: NormalizedPath, requireOpen?: boolean): boolean { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index c090b4b14d0..68c448a7e76 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -64,12 +64,22 @@ namespace ts.server { this.switchToScriptVersionCache(); } + private resetSourceMapInfo() { + this.info.sourceFileLike = undefined; + this.info.closeSourceMapFileWatcher(); + this.info.sourceMapFilePath = undefined; + this.info.declarationInfoPath = undefined; + this.info.sourceInfos = undefined; + this.info.documentPositionMapper = undefined; + } + /** Public for testing */ public useText(newText?: string) { this.svc = undefined; this.text = newText; this.lineMap = undefined; this.fileSize = undefined; + this.resetSourceMapInfo(); this.version.text++; } @@ -79,6 +89,7 @@ namespace ts.server { this.text = undefined; this.lineMap = undefined; this.fileSize = undefined; + this.resetSourceMapInfo(); } /** @@ -156,8 +167,8 @@ namespace ts.server { : ScriptSnapshot.fromString(this.getOrLoadText()); } - public getLineInfo(line: number): AbsolutePositionAndLineText { - return this.switchToScriptVersionCache().getLineInfo(line); + public getAbsolutePositionAndLineText(line: number): AbsolutePositionAndLineText { + return this.switchToScriptVersionCache().getAbsolutePositionAndLineText(line); } /** * @param line 0 based index @@ -176,9 +187,9 @@ namespace ts.server { * @param line 1 based index * @param offset 1 based index */ - lineOffsetToPosition(line: number, offset: number): number { + lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number { if (!this.useScriptVersionCacheIfValidOrOpen()) { - return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text); + return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text, allowEdits); } // TODO: assert this offset is actually on the line @@ -246,6 +257,17 @@ namespace ts.server { Debug.assert(!this.svc, "ScriptVersionCache should not be set"); return this.lineMap || (this.lineMap = computeLineStarts(this.getOrLoadText())); } + + getLineInfo(): LineInfo { + if (this.svc) { + return { + getLineCount: () => this.svc!.getLineCount(), + getLineText: line => this.svc!.getAbsolutePositionAndLineText(line + 1).lineText! + }; + } + const lineMap = this.getLineMap(); + return getLineInfo(this.text!, lineMap); + } } /*@internal*/ @@ -259,6 +281,12 @@ namespace ts.server { sourceFile: SourceFile; } + /*@internal*/ + export interface SourceMapFileWatcher { + watcher: FileWatcher; + sourceInfos?: Map; + } + export class ScriptInfo { /** * All projects that include this file @@ -284,6 +312,20 @@ namespace ts.server { /*@internal*/ mTime?: number; + /*@internal*/ + sourceFileLike?: SourceFileLike; + + /*@internal*/ + sourceMapFilePath?: Path | SourceMapFileWatcher | false; + + // Present on sourceMapFile info + /*@internal*/ + declarationInfoPath?: Path; + /*@internal*/ + sourceInfos?: Map; + /*@internal*/ + documentPositionMapper?: DocumentPositionMapper | false; + constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, @@ -521,8 +563,8 @@ namespace ts.server { } /*@internal*/ - getLineInfo(line: number): AbsolutePositionAndLineText { - return this.textStorage.getLineInfo(line); + getAbsolutePositionAndLineText(line: number): AbsolutePositionAndLineText { + return this.textStorage.getAbsolutePositionAndLineText(line); } editContent(start: number, end: number, newText: string): void { @@ -551,8 +593,12 @@ namespace ts.server { * @param line 1 based index * @param offset 1 based index */ - lineOffsetToPosition(line: number, offset: number): number { - return this.textStorage.lineOffsetToPosition(line, offset); + lineOffsetToPosition(line: number, offset: number): number; + /*@internal*/ + // tslint:disable-next-line:unified-signatures + lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number; + lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number { + return this.textStorage.lineOffsetToPosition(line, offset, allowEdits); } positionToLineOffset(position: number): protocol.Location { @@ -562,5 +608,18 @@ namespace ts.server { public isJavaScript() { return this.scriptKind === ScriptKind.JS || this.scriptKind === ScriptKind.JSX; } + + /*@internal*/ + getLineInfo(): LineInfo { + return this.textStorage.getLineInfo(); + } + + /*@internal*/ + closeSourceMapFileWatcher() { + if (this.sourceMapFilePath && !isString(this.sourceMapFilePath)) { + closeFileWatcherOf(this.sourceMapFilePath); + this.sourceMapFilePath = undefined; + } + } } } diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index a350ed99f71..afd8f67ecbe 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -308,8 +308,8 @@ namespace ts.server { return this._getSnapshot().version; } - getLineInfo(line: number): AbsolutePositionAndLineText { - return this._getSnapshot().index.lineNumberToInfo(line); + getAbsolutePositionAndLineText(oneBasedLine: number): AbsolutePositionAndLineText { + return this._getSnapshot().index.lineNumberToInfo(oneBasedLine); } lineOffsetToPosition(line: number, column: number): number { @@ -348,6 +348,10 @@ namespace ts.server { } } + getLineCount() { + return this._getSnapshot().index.getLineCount(); + } + static fromString(script: string) { const svc = new ScriptVersionCache(); const snap = new LineIndexSnapshot(0, svc, new LineIndex()); @@ -400,8 +404,12 @@ namespace ts.server { return this.root.charOffsetToLineInfo(1, position); } + getLineCount() { + return this.root.lineCount(); + } + lineNumberToInfo(oneBasedLine: number): AbsolutePositionAndLineText { - const lineCount = this.root.lineCount(); + const lineCount = this.getLineCount(); if (oneBasedLine <= lineCount) { const { position, leaf } = this.root.lineNumberToInfo(oneBasedLine, 0); return { absolutePosition: position, lineText: leaf && leaf.text }; diff --git a/src/server/session.ts b/src/server/session.ts index 0b9ad913f8a..d534f445ee3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1474,7 +1474,7 @@ namespace ts.server { // only to the previous line. If all this is true, then // add edits necessary to properly indent the current line. if ((args.key === "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { - const { lineText, absolutePosition } = scriptInfo.getLineInfo(args.line); + const { lineText, absolutePosition } = scriptInfo.getAbsolutePositionAndLineText(args.line); if (lineText && lineText.search("\\S") < 0) { const preferredIndent = languageService.getIndentationAtPosition(file, position, formatOptions); let hasIndent = 0; diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index eec693bbb69..461771c0b5e 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -97,7 +97,7 @@ namespace ts.codefix { optional: boolean, body: Block | undefined, ): MethodDeclaration | undefined { - const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.SuppressAnyReturnType); + const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType); if (!signatureDeclaration) { return undefined; } diff --git a/src/services/completions.ts b/src/services/completions.ts index 43f00c58acf..bbcee4dd47e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -913,7 +913,7 @@ namespace ts.Completions { } else { for (const symbol of type.getApparentProperties()) { - if (typeChecker.isValidPropertyAccessForCompletions(node.kind === SyntaxKind.ImportType ? node : node.parent, type, symbol)) { + if (typeChecker.isValidPropertyAccessForCompletions(node.kind === SyntaxKind.ImportType ? node : node.parent, type, symbol)) { addPropertySymbol(symbol); } } diff --git a/src/services/navigationBar.ts b/src/services/navigationBar.ts index 760d6e71031..60f1283f7e6 100644 --- a/src/services/navigationBar.ts +++ b/src/services/navigationBar.ts @@ -660,7 +660,7 @@ namespace ts.NavigationBar { else if (isCallExpression(parent)) { const name = getCalledExpressionName(parent.expression); if (name !== undefined) { - const args = mapDefined(parent.arguments, a => isStringLiteral(a) ? a.getText(curSourceFile) : undefined).join(", "); + const args = mapDefined(parent.arguments, a => isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", "); return `${name}(${args}) callback`; } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 9c504357102..c2a7ea180bb 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -28,12 +28,12 @@ namespace ts.OrganizeImports { organizeImportsWorker(topLevelExportDecls, coalesceExports); for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { - const ambientModuleBody = getModuleBlock(ambientModule as ModuleDeclaration)!; // TODO: GH#18217 + if (!ambientModule.body) { continue; } - const ambientModuleImportDecls = ambientModuleBody.statements.filter(isImportDeclaration); + const ambientModuleImportDecls = ambientModule.body.statements.filter(isImportDeclaration); organizeImportsWorker(ambientModuleImportDecls, coalesceAndOrganizeImports); - const ambientModuleExportDecls = ambientModuleBody.statements.filter(isExportDeclaration); + const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); organizeImportsWorker(ambientModuleExportDecls, coalesceExports); } @@ -81,11 +81,6 @@ namespace ts.OrganizeImports { } } - function getModuleBlock(moduleDecl: ModuleDeclaration): ModuleBlock | undefined { - const body = moduleDecl.body; - return body && !isIdentifier(body) ? (isModuleBlock(body) ? body : getModuleBlock(body)) : undefined; - } - function removeUnusedImports(oldImports: ReadonlyArray, sourceFile: SourceFile, program: Program) { const typeChecker = program.getTypeChecker(); const jsxNamespace = typeChecker.getJsxNamespace(); diff --git a/src/services/outliningElementsCollector.ts b/src/services/outliningElementsCollector.ts index 9d2c61a1a7d..6f834512f0b 100644 --- a/src/services/outliningElementsCollector.ts +++ b/src/services/outliningElementsCollector.ts @@ -37,6 +37,10 @@ namespace ts.OutliningElementsCollector { addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out); } + if (isFunctionExpressionAssignedToVariable(n)) { + addOutliningForLeadingCommentsForNode(n.parent.parent.parent, sourceFile, cancellationToken, out); + } + const span = getOutliningSpanForNode(n, sourceFile); if (span) out.push(span); @@ -54,6 +58,14 @@ namespace ts.OutliningElementsCollector { } depthRemaining++; } + + function isFunctionExpressionAssignedToVariable(n: Node) { + if (!isFunctionExpression(n) && !isArrowFunction(n)) { + return false; + } + const ancestor = findAncestor(n, isVariableStatement); + return !!ancestor && getSingleInitializerOfVariableStatementOrPropertyDeclaration(ancestor) === n; + } } function addRegionOutliningSpans(sourceFile: SourceFile, out: Push): void { @@ -175,6 +187,7 @@ namespace ts.OutliningElementsCollector { case SyntaxKind.ModuleBlock: return spanForNode(n.parent); case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.CaseBlock: @@ -206,10 +219,10 @@ namespace ts.OutliningElementsCollector { } function spanForObjectOrArrayLiteral(node: Node, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined { - // If the block has no leading keywords and is inside an array literal, + // If the block has no leading keywords and is inside an array literal or call expression, // we only want to collapse the span of the block. // Otherwise, the collapsed section will include the end of the previous line. - return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent), open); + return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent) && !isCallExpression(node.parent), open); } function spanForNode(hintSpanNode: Node, autoCollapse = false, useFullStart = true, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined { diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index c1915a66431..823bd640a59 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -449,8 +449,7 @@ namespace ts.refactor.extractSymbol { case SyntaxKind.ThisKeyword: rangeFacts |= RangeFacts.UsesThis; break; - case SyntaxKind.LabeledStatement: - { + case SyntaxKind.LabeledStatement: { const label = (node).label; (seenLabels || (seenLabels = [])).push(label.escapedText); forEachChild(node, visit); @@ -458,8 +457,7 @@ namespace ts.refactor.extractSymbol { break; } case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - { + case SyntaxKind.ContinueStatement: { const label = (node).label; if (label) { if (!contains(seenLabels, label.escapedText)) { diff --git a/src/services/services.ts b/src/services/services.ts index 20400a3ec47..15f9709cb38 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -602,8 +602,8 @@ namespace ts { return getLineStarts(this); } - public getPositionOfLineAndCharacter(line: number, character: number): number { - return getPositionOfLineAndCharacter(this, line, character); + public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number { + return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text, allowEdits); } public getLineEndOfPosition(pos: number): number { @@ -1139,7 +1139,16 @@ namespace ts { const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - const sourceMapper = getSourceMapper(useCaseSensitiveFileNames, currentDirectory, log, host, () => program); + const sourceMapper = getSourceMapper({ + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getCurrentDirectory: () => currentDirectory, + getProgram, + fileExists: host.fileExists && (f => host.fileExists!(f)), + readFile: host.readFile && ((f, encoding) => host.readFile!(f, encoding)), + getDocumentPositionMapper: host.getDocumentPositionMapper && ((generatedFileName, sourceFileName) => host.getDocumentPositionMapper!(generatedFileName, sourceFileName)), + getSourceFileLike: host.getSourceFileLike && (f => host.getSourceFileLike!(f)), + log + }); function getValidSourceFile(fileName: string): SourceFile { const sourceFile = program.getSourceFile(fileName); @@ -1203,15 +1212,7 @@ namespace ts { writeFile: noop, getCurrentDirectory: () => currentDirectory, fileExists, - readFile(fileName) { - // stub missing host functionality - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - if (entry) { - return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot); - } - return host.readFile && host.readFile(fileName); - }, + readFile, realpath: host.realpath && (path => host.realpath!(path)), directoryExists: directoryName => { return directoryProbablyExists(directoryName, host); @@ -1272,6 +1273,16 @@ namespace ts { (!!host.fileExists && host.fileExists(fileName)); } + function readFile(fileName: string) { + // stub missing host functionality + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache && hostCache.getEntryByPath(path); + if (entry) { + return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot); + } + return host.readFile && host.readFile(fileName); + } + // Release any files we have acquired in the old program but are // not part of the new program. function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { diff --git a/src/services/sourcemaps.ts b/src/services/sourcemaps.ts index 92ebcc8c261..d07c21a9f43 100644 --- a/src/services/sourcemaps.ts +++ b/src/services/sourcemaps.ts @@ -9,151 +9,182 @@ namespace ts { clearCache(): void; } - export function getSourceMapper( - useCaseSensitiveFileNames: boolean, - currentDirectory: string, - log: (message: string) => void, - host: LanguageServiceHost, - getProgram: () => Program, - ): SourceMapper { - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - let sourcemappedFileCache: SourceFileLikeCache; + export interface SourceMapperHost { + useCaseSensitiveFileNames(): boolean; + getCurrentDirectory(): string; + getProgram(): Program | undefined; + fileExists?(path: string): boolean; + readFile?(path: string, encoding?: string): string | undefined; + getSourceFileLike?(fileName: string): SourceFileLike | undefined; + getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; + log(s: string): void; + } + + export function getSourceMapper(host: SourceMapperHost): SourceMapper { + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + const currentDirectory = host.getCurrentDirectory(); + const sourceFileLike = createMap(); + const documentPositionMappers = createMap(); return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; function toPath(fileName: string) { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } - function scanForSourcemapURL(fileName: string) { - const mappedFile = sourcemappedFileCache.get(toPath(fileName)); - if (!mappedFile) { - return; - } + function getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string) { + const path = toPath(generatedFileName); + const value = documentPositionMappers.get(path); + if (value) return value; - return tryGetSourceMappingURL(mappedFile.text, getLineStarts(mappedFile)); - } - - function convertDocumentToSourceMapper(file: { sourceMapper?: DocumentPositionMapper }, contents: string, mapFileName: string) { - const map = tryParseRawSourceMap(contents); - if (!map || !map.sources || !map.file || !map.mappings) { - // obviously invalid map - return file.sourceMapper = identitySourceMapConsumer; + let mapper: DocumentPositionMapper | undefined; + if (host.getDocumentPositionMapper) { + mapper = host.getDocumentPositionMapper(generatedFileName, sourceFileName); } - const program = getProgram(); - return file.sourceMapper = createDocumentPositionMapper({ - getSourceFileLike: s => { - // Lookup file in program, if provided - const file = program && program.getSourceFileByPath(s); - // file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file - if (file === undefined || file.resolvedPath !== s) { - // Otherwise check the cache (which may hit disk) - return sourcemappedFileCache.get(s); - } - return file; - }, - getCanonicalFileName, - log, - }, map, mapFileName); - } - - function getSourceMapper(fileName: string, file: SourceFileLike): DocumentPositionMapper { - if (!host.readFile || !host.fileExists) { - return file.sourceMapper = identitySourceMapConsumer; + else if (host.readFile) { + const file = getSourceFileLike(generatedFileName); + mapper = file && ts.getDocumentPositionMapper( + { getSourceFileLike, getCanonicalFileName, log: s => host.log(s) }, + generatedFileName, + getLineInfo(file.text, getLineStarts(file)), + f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined + ); } - if (file.sourceMapper) { - return file.sourceMapper; - } - let mapFileName = scanForSourcemapURL(fileName); - if (mapFileName) { - const match = base64UrlRegExp.exec(mapFileName); - if (match) { - if (match[1]) { - const base64Object = match[1]; - return convertDocumentToSourceMapper(file, base64decode(sys, base64Object), fileName); - } - // Not a data URL we can parse, skip it - mapFileName = undefined; - } - } - const possibleMapLocations: string[] = []; - if (mapFileName) { - possibleMapLocations.push(mapFileName); - } - possibleMapLocations.push(fileName + ".map"); - for (const location of possibleMapLocations) { - const mapPath = ts.toPath(location, getDirectoryPath(fileName), getCanonicalFileName); - if (host.fileExists(mapPath)) { - return convertDocumentToSourceMapper(file, host.readFile(mapPath)!, mapPath); // TODO: GH#18217 - } - } - return file.sourceMapper = identitySourceMapConsumer; + documentPositionMappers.set(path, mapper || identitySourceMapConsumer); + return mapper || identitySourceMapConsumer; } function tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined { if (!isDeclarationFileName(info.fileName)) return undefined; - const file = getFile(info.fileName); + const file = getSourceFile(info.fileName); if (!file) return undefined; - const newLoc = getSourceMapper(info.fileName, file).getSourcePosition(info); - return newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; + + const newLoc = getDocumentPositionMapper(info.fileName).getSourcePosition(info); + return !newLoc || newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; } function tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined { - const program = getProgram(); + if (isDeclarationFileName(info.fileName)) return undefined; + + const sourceFile = getSourceFile(info.fileName); + if (!sourceFile) return undefined; + + const program = host.getProgram()!; const options = program.getCompilerOptions(); const outPath = options.outFile || options.out; + const declarationPath = outPath ? removeFileExtension(outPath) + Extension.Dts : getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); if (declarationPath === undefined) return undefined; - const declarationFile = getFile(declarationPath); - if (!declarationFile) return undefined; - const newLoc = getSourceMapper(declarationPath, declarationFile).getGeneratedPosition(info); + + const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info); return newLoc === info ? undefined : newLoc; } - function getFile(fileName: string): SourceFileLike | undefined { + function getSourceFile(fileName: string) { + const program = host.getProgram(); + if (!program) return undefined; + const path = toPath(fileName); - const file = getProgram().getSourceFileByPath(path); - if (file && file.resolvedPath === path) { - return file; + // file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file + const file = program.getSourceFileByPath(path); + return file && file.resolvedPath === path ? file : undefined; + } + + function getOrCreateSourceFileLike(fileName: string): SourceFileLike | undefined { + const path = toPath(fileName); + const fileFromCache = sourceFileLike.get(path); + if (fileFromCache !== undefined) return fileFromCache ? fileFromCache : undefined; + + if (!host.readFile || host.fileExists && !host.fileExists(path)) { + sourceFileLike.set(path, false); + return undefined; } - return sourcemappedFileCache.get(path); + + // And failing that, check the disk + const text = host.readFile(path); + const file = text ? createSourceFileLike(text) : false; + sourceFileLike.set(path, file); + return file ? file : undefined; + } + + // This can be called from source mapper in either source program or program that includes generated file + function getSourceFileLike(fileName: string) { + return !host.getSourceFileLike ? + getSourceFile(fileName) || getOrCreateSourceFileLike(fileName) : + host.getSourceFileLike(fileName); } function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { - const file = getFile(fileName)!; // TODO: GH#18217 + const file = getSourceFileLike(fileName)!; // TODO: GH#18217 return file.getLineAndCharacterOfPosition(position); } function clearCache(): void { - sourcemappedFileCache = createSourceFileLikeCache(host); + sourceFileLike.clear(); + documentPositionMappers.clear(); } } - interface SourceFileLikeCache { - get(path: Path): SourceFileLike | undefined; + /** + * string | undefined to contents of map file to create DocumentPositionMapper from it + * DocumentPositionMapper | false to give back cached DocumentPositionMapper + */ + export type ReadMapFile = (mapFileName: string, mapFileNameFromDts: string | undefined) => string | undefined | DocumentPositionMapper | false; + + export function getDocumentPositionMapper( + host: DocumentPositionMapperHost, + generatedFileName: string, + generatedFileLineInfo: LineInfo, + readMapFile: ReadMapFile) { + let mapFileName = tryGetSourceMappingURL(generatedFileLineInfo); + if (mapFileName) { + const match = base64UrlRegExp.exec(mapFileName); + if (match) { + if (match[1]) { + const base64Object = match[1]; + return convertDocumentToSourceMapper(host, base64decode(sys, base64Object), generatedFileName); + } + // Not a data URL we can parse, skip it + mapFileName = undefined; + } + } + const possibleMapLocations: string[] = []; + if (mapFileName) { + possibleMapLocations.push(mapFileName); + } + possibleMapLocations.push(generatedFileName + ".map"); + const originalMapFileName = mapFileName && getNormalizedAbsolutePath(mapFileName, getDirectoryPath(generatedFileName)); + for (const location of possibleMapLocations) { + const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(generatedFileName)); + const mapFileContents = readMapFile(mapFileName, originalMapFileName); + if (isString(mapFileContents)) { + return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); + } + if (mapFileContents !== undefined) { + return mapFileContents || undefined; + } + } + return undefined; } - function createSourceFileLikeCache(host: { readFile?: (path: string) => string | undefined, fileExists?: (path: string) => boolean }): SourceFileLikeCache { - const cached = createMap(); + function convertDocumentToSourceMapper(host: DocumentPositionMapperHost, contents: string, mapFileName: string) { + const map = tryParseRawSourceMap(contents); + if (!map || !map.sources || !map.file || !map.mappings) { + // obviously invalid map + return undefined; + } + + return createDocumentPositionMapper(host, map, mapFileName); + } + + function createSourceFileLike(text: string, lineMap?: SourceFileLike["lineMap"]): SourceFileLike { return { - get(path: Path) { - if (cached.has(path)) { - return cached.get(path); - } - if (!host.fileExists || !host.readFile || !host.fileExists(path)) return; - // And failing that, check the disk - const text = host.readFile(path)!; // TODO: GH#18217 - const file = { - text, - lineMap: undefined, - getLineAndCharacterOfPosition(pos: number) { - return computeLineAndCharacterOfPosition(getLineStarts(this), pos); - } - } as SourceFileLike; - cached.set(path, file); - return file; + text, + lineMap, + getLineAndCharacterOfPosition(pos: number) { + return computeLineAndCharacterOfPosition(getLineStarts(this), pos); } }; } diff --git a/src/services/suggestionDiagnostics.ts b/src/services/suggestionDiagnostics.ts index 5c175146e34..63691a72c8b 100644 --- a/src/services/suggestionDiagnostics.ts +++ b/src/services/suggestionDiagnostics.ts @@ -1,5 +1,7 @@ /* @internal */ namespace ts { + const visitedNestedConvertibleFunctions = createMap(); + export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] { program.getSemanticDiagnostics(sourceFile, cancellationToken); const diags: DiagnosticWithLocation[] = []; @@ -13,6 +15,7 @@ namespace ts { const isJsFile = isSourceFileJS(sourceFile); + visitedNestedConvertibleFunctions.clear(); check(sourceFile); if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) { @@ -114,17 +117,22 @@ namespace ts { } function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push): void { - if (!isAsyncFunction(node) && - node.body && - isBlock(node.body) && - hasReturnStatementWithPromiseHandler(node.body) && - returnsPromise(node, checker)) { + // need to check function before checking map so that deeper levels of nested callbacks are checked + if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) { diags.push(createDiagnosticForNode( !node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node, Diagnostics.This_may_be_converted_to_an_async_function)); } } + function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) { + return !isAsyncFunction(node) && + node.body && + isBlock(node.body) && + hasReturnStatementWithPromiseHandler(node.body) && + returnsPromise(node, checker); + } + function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean { const functionType = checker.getTypeAtLocation(node); const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call); @@ -169,14 +177,20 @@ namespace ts { // should be kept up to date with getTransformationBody in convertToAsyncFunction.ts function isFixablePromiseArgument(arg: Expression): boolean { switch (arg.kind) { - case SyntaxKind.NullKeyword: - case SyntaxKind.Identifier: // identifier includes undefined case SyntaxKind.FunctionDeclaration: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: + visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true); + /* falls through */ + case SyntaxKind.NullKeyword: + case SyntaxKind.Identifier: // identifier includes undefined return true; default: return false; } } + + function getKeyFromNode(exp: FunctionLikeDeclaration) { + return `${exp.pos.toString()}:${exp.end.toString()}`; + } } diff --git a/src/services/types.ts b/src/services/types.ts index 69f07b705a7..5100a999306 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -91,7 +91,6 @@ namespace ts { export interface SourceFileLike { getLineAndCharacterOfPosition(pos: number): LineAndCharacter; - /*@internal*/ sourceMapper?: DocumentPositionMapper; } export interface SourceMapSource { @@ -233,6 +232,11 @@ namespace ts { installPackage?(options: InstallPackageOptions): Promise; /* @internal */ inspectValue?(options: InspectValueOptions): Promise; writeFile?(fileName: string, content: string): void; + + /* @internal */ + getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; + /* @internal */ + getSourceFileLike?(fileName: string): SourceFileLike | undefined; } /* @internal */ diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 22e6b31b55c..2f78ef2324b 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1282,11 +1282,11 @@ namespace ts { return !!compilerOptions.module || compilerOptions.target! >= ScriptTarget.ES2015 || !!compilerOptions.noEmit; } - export function hostUsesCaseSensitiveFileNames(host: LanguageServiceHost): boolean { + export function hostUsesCaseSensitiveFileNames(host: { useCaseSensitiveFileNames?(): boolean; }): boolean { return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false; } - export function hostGetCanonicalFileName(host: LanguageServiceHost): GetCanonicalFileName { + export function hostGetCanonicalFileName(host: { useCaseSensitiveFileNames?(): boolean; }): GetCanonicalFileName { return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 32772ed311b..2e5341732b5 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -36,64 +36,109 @@ "runner.ts", - "unittests/extractTestHelpers.ts", - "unittests/tsserverProjectSystem.ts", - "unittests/typingsInstaller.ts", + "unittests/services/extract/helpers.ts", + "unittests/tscWatch/helpers.ts", + "unittests/tsserver/helpers.ts", "unittests/asserts.ts", "unittests/base64.ts", "unittests/builder.ts", - "unittests/cancellableLanguageServiceOperations.ts", - "unittests/commandLineParsing.ts", - "unittests/compileOnSave.ts", "unittests/compilerCore.ts", - "unittests/configurationExtension.ts", - "unittests/convertCompilerOptionsFromJson.ts", - "unittests/convertToAsyncFunction.ts", "unittests/convertToBase64.ts", - "unittests/convertTypeAcquisitionFromJson.ts", "unittests/customTransforms.ts", - "unittests/extractConstants.ts", - "unittests/extractFunctions.ts", - "unittests/extractRanges.ts", "unittests/factory.ts", - "unittests/hostNewLineSupport.ts", "unittests/incrementalParser.ts", - "unittests/initializeTSConfig.ts", "unittests/jsDocParsing.ts", - "unittests/languageService.ts", - "unittests/matchFiles.ts", "unittests/moduleResolution.ts", - "unittests/organizeImports.ts", "unittests/parsePseudoBigInt.ts", "unittests/paths.ts", "unittests/printer.ts", - "unittests/programMissingFiles.ts", - "unittests/programNoParseFalsyFileNames.ts", - "unittests/projectErrors.ts", - "unittests/projectReferences.ts", + "unittests/programApi.ts", "unittests/publicApi.ts", "unittests/reuseProgramStructure.ts", - "unittests/session.ts", "unittests/semver.ts", - "unittests/showConfig.ts", - "unittests/symbolWalker.ts", - "unittests/telemetry.ts", - "unittests/textChanges.ts", - "unittests/textStorage.ts", "unittests/transform.ts", - "unittests/transpile.ts", "unittests/tsbuild.ts", "unittests/tsbuildWatchMode.ts", - "unittests/tsconfigParsing.ts", - "unittests/tscWatchMode.ts", - "unittests/versionCache.ts", + "unittests/config/commandLineParsing.ts", + "unittests/config/configurationExtension.ts", + "unittests/config/convertCompilerOptionsFromJson.ts", + "unittests/config/convertTypeAcquisitionFromJson.ts", + "unittests/config/initializeTSConfig.ts", + "unittests/config/matchFiles.ts", + "unittests/config/projectReferences.ts", + "unittests/config/showConfig.ts", + "unittests/config/tsconfigParsing.ts", "unittests/evaluation/asyncArrow.ts", "unittests/evaluation/asyncGenerator.ts", "unittests/evaluation/forAwaitOf.ts", + "unittests/services/cancellableLanguageServiceOperations.ts", "unittests/services/colorization.ts", + "unittests/services/convertToAsyncFunction.ts", "unittests/services/documentRegistry.ts", + "unittests/services/extract/constants.ts", + "unittests/services/extract/functions.ts", + "unittests/services/extract/symbolWalker.ts", + "unittests/services/extract/ranges.ts", + "unittests/services/hostNewLineSupport.ts", + "unittests/services/languageService.ts", + "unittests/services/organizeImports.ts", "unittests/services/patternMatcher.ts", - "unittests/services/preProcessFile.ts" + "unittests/services/preProcessFile.ts", + "unittests/services/textChanges.ts", + "unittests/services/transpile.ts", + "unittests/tscWatch/consoleClearing.ts", + "unittests/tscWatch/emit.ts", + "unittests/tscWatch/emitAndErrorUpdates.ts", + "unittests/tscWatch/programUpdates.ts", + "unittests/tscWatch/resolutionCache.ts", + "unittests/tscWatch/watchEnvironment.ts", + "unittests/tscWatch/watchApi.ts", + "unittests/tsserver/cachingFileSystemInformation.ts", + "unittests/tsserver/cancellationToken.ts", + "unittests/tsserver/compileOnSave.ts", + "unittests/tsserver/completions.ts", + "unittests/tsserver/configFileSearch.ts", + "unittests/tsserver/configuredProjects.ts", + "unittests/tsserver/declarationFileMaps.ts", + "unittests/tsserver/documentRegistry.ts", + "unittests/tsserver/duplicatePackages.ts", + "unittests/tsserver/events/largeFileReferenced.ts", + "unittests/tsserver/events/projectLanguageServiceState.ts", + "unittests/tsserver/events/projectLoading.ts", + "unittests/tsserver/events/projectUpdatedInBackground.ts", + "unittests/tsserver/events/surveyReady.ts", + "unittests/tsserver/externalProjects.ts", + "unittests/tsserver/forceConsistentCasingInFileNames.ts", + "unittests/tsserver/formatSettings.ts", + "unittests/tsserver/getApplicableRefactors.ts", + "unittests/tsserver/getEditsForFileRename.ts", + "unittests/tsserver/importHelpers.ts", + "unittests/tsserver/inferredProjects.ts", + "unittests/tsserver/languageService.ts", + "unittests/tsserver/maxNodeModuleJsDepth.ts", + "unittests/tsserver/metadataInResponse.ts", + "unittests/tsserver/navTo.ts", + "unittests/tsserver/occurences.ts", + "unittests/tsserver/openFile.ts", + "unittests/tsserver/projectErrors.ts", + "unittests/tsserver/projectReferences.ts", + "unittests/tsserver/projects.ts", + "unittests/tsserver/refactors.ts", + "unittests/tsserver/reload.ts", + "unittests/tsserver/rename.ts", + "unittests/tsserver/resolutionCache.ts", + "unittests/tsserver/session.ts", + "unittests/tsserver/skipLibCheck.ts", + "unittests/tsserver/symLinks.ts", + "unittests/tsserver/syntaxOperations.ts", + "unittests/tsserver/textStorage.ts", + "unittests/tsserver/telemetry.ts", + "unittests/tsserver/typeAquisition.ts", + "unittests/tsserver/typeReferenceDirectives.ts", + "unittests/tsserver/typingsInstaller.ts", + "unittests/tsserver/untitledFiles.ts", + "unittests/tsserver/versionCache.ts", + "unittests/tsserver/watchEnvironment.ts" ] } diff --git a/src/testRunner/unittests/asserts.ts b/src/testRunner/unittests/asserts.ts index 8d5138085a8..c54173e2f90 100644 --- a/src/testRunner/unittests/asserts.ts +++ b/src/testRunner/unittests/asserts.ts @@ -1,5 +1,5 @@ namespace ts { - describe("assert", () => { + describe("unittests:: assert", () => { it("deepEqual", () => { assert.throws(() => assert.deepEqual(createNodeArray([createIdentifier("A")]), createNodeArray([createIdentifier("B")]))); assert.throws(() => assert.deepEqual(createNodeArray([], /*hasTrailingComma*/ true), createNodeArray([], /*hasTrailingComma*/ false))); diff --git a/src/testRunner/unittests/base64.ts b/src/testRunner/unittests/base64.ts index 11c2623df65..1dc51a8fbf5 100644 --- a/src/testRunner/unittests/base64.ts +++ b/src/testRunner/unittests/base64.ts @@ -1,5 +1,5 @@ namespace ts { - describe("base64", () => { + describe("unittests:: base64", () => { describe("base64decode", () => { it("can decode input strings correctly without needing a host implementation", () => { const tests = [ diff --git a/src/testRunner/unittests/builder.ts b/src/testRunner/unittests/builder.ts index 4a163d6b56b..709222a4dff 100644 --- a/src/testRunner/unittests/builder.ts +++ b/src/testRunner/unittests/builder.ts @@ -1,5 +1,5 @@ namespace ts { - describe("builder", () => { + describe("unittests:: builder", () => { it("emits dependent files", () => { const files: NamedSourceText[] = [ { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, diff --git a/src/testRunner/unittests/compilerCore.ts b/src/testRunner/unittests/compilerCore.ts index 27f5cdc0887..4c3918c3149 100644 --- a/src/testRunner/unittests/compilerCore.ts +++ b/src/testRunner/unittests/compilerCore.ts @@ -1,5 +1,5 @@ namespace ts { - describe("compilerCore", () => { + describe("unittests:: compilerCore", () => { describe("equalOwnProperties", () => { it("correctly equates objects", () => { assert.isTrue(equalOwnProperties({}, {})); diff --git a/src/testRunner/unittests/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts similarity index 97% rename from src/testRunner/unittests/commandLineParsing.ts rename to src/testRunner/unittests/config/commandLineParsing.ts index 7e2090eb3bd..25fa3a45050 100644 --- a/src/testRunner/unittests/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("parseCommandLine", () => { + describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) { const parsed = parseCommandLine(commandLine); @@ -367,7 +367,7 @@ namespace ts { }); }); - describe("parseBuildOptions", () => { + describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { const parsed = parseBuildCommand(commandLine); const parsedBuildOptions = JSON.stringify(parsed.buildOptions); diff --git a/src/testRunner/unittests/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts similarity index 97% rename from src/testRunner/unittests/configurationExtension.ts rename to src/testRunner/unittests/config/configurationExtension.ts index 57c899eb5a8..25e975c0fb2 100644 --- a/src/testRunner/unittests/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -208,7 +208,7 @@ namespace ts { } } - describe("configurationExtension", () => { + describe("unittests:: config:: configurationExtension", () => { forEach<[string, string, fakes.ParseConfigHost], void>([ ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] diff --git a/src/testRunner/unittests/convertCompilerOptionsFromJson.ts b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts similarity index 97% rename from src/testRunner/unittests/convertCompilerOptionsFromJson.ts rename to src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts index 7b8af54f930..318bc0ceb26 100644 --- a/src/testRunner/unittests/convertCompilerOptionsFromJson.ts +++ b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts @@ -1,5 +1,5 @@ namespace ts { - describe("convertCompilerOptionsFromJson", () => { + describe("unittests:: config:: convertCompilerOptionsFromJson", () => { const formatDiagnosticHost: FormatDiagnosticsHost = { getCurrentDirectory: () => "/apath/", getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), diff --git a/src/testRunner/unittests/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts similarity index 96% rename from src/testRunner/unittests/convertTypeAcquisitionFromJson.ts rename to src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts index b46d37f0428..890cad4ade6 100644 --- a/src/testRunner/unittests/convertTypeAcquisitionFromJson.ts +++ b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts @@ -1,6 +1,6 @@ namespace ts { interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; } - describe("convertTypeAcquisitionFromJson", () => { + describe("unittests:: config:: convertTypeAcquisitionFromJson", () => { function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { assertTypeAcquisitionWithJson(json, configFileName, expectedResult); assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); diff --git a/src/testRunner/unittests/initializeTSConfig.ts b/src/testRunner/unittests/config/initializeTSConfig.ts similarity index 95% rename from src/testRunner/unittests/initializeTSConfig.ts rename to src/testRunner/unittests/config/initializeTSConfig.ts index 679eecf71b1..f7fb7d2a439 100644 --- a/src/testRunner/unittests/initializeTSConfig.ts +++ b/src/testRunner/unittests/config/initializeTSConfig.ts @@ -1,5 +1,5 @@ namespace ts { - describe("initTSConfig", () => { + describe("unittests:: config:: initTSConfig", () => { function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { describe(name, () => { const commandLine = parseCommandLine(commandLinesArgs); @@ -30,4 +30,4 @@ namespace ts { initTSConfigCorrectly("Initialized TSConfig with advanced options", ["--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/matchFiles.ts b/src/testRunner/unittests/config/matchFiles.ts similarity index 97% rename from src/testRunner/unittests/matchFiles.ts rename to src/testRunner/unittests/config/matchFiles.ts index 5ece71245bb..a30fa468443 100644 --- a/src/testRunner/unittests/matchFiles.ts +++ b/src/testRunner/unittests/config/matchFiles.ts @@ -143,7 +143,7 @@ namespace ts { return createFileDiagnostic(file, start, length, diagnosticMessage, arg0); } - describe("matchFiles", () => { + describe("unittests:: config:: matchFiles", () => { it("with defaults", () => { const json = {}; const expected: ParsedCommandLine = { diff --git a/src/testRunner/unittests/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts similarity index 93% rename from src/testRunner/unittests/projectReferences.ts rename to src/testRunner/unittests/config/projectReferences.ts index 5b99e01585d..266b016c681 100644 --- a/src/testRunner/unittests/projectReferences.ts +++ b/src/testRunner/unittests/config/projectReferences.ts @@ -96,7 +96,7 @@ namespace ts { checkResult(prog, host); } - describe("project-references meta check", () => { + describe("unittests:: config:: project-references meta check", () => { it("default setup was created correctly", () => { const spec: TestSpecification = { "/primary": { @@ -118,7 +118,7 @@ namespace ts { /** * Validate that we enforce the basic settings constraints for referenced projects */ - describe("project-references constraint checking for settings", () => { + describe("unittests:: config:: project-references constraint checking for settings", () => { it("errors when declaration = false", () => { const spec: TestSpecification = { "/primary": { @@ -248,7 +248,7 @@ namespace ts { /** * Path mapping behavior */ - describe("project-references path mapping", () => { + describe("unittests:: config:: project-references path mapping", () => { it("redirects to the output .d.ts file", () => { const spec: TestSpecification = { "/alpha": { @@ -268,7 +268,7 @@ namespace ts { }); }); - describe("project-references nice-behavior", () => { + describe("unittests:: config:: project-references nice-behavior", () => { it("issues a nice error when the input file is missing", () => { const spec: TestSpecification = { "/alpha": { @@ -289,7 +289,7 @@ namespace ts { /** * 'composite' behavior */ - describe("project-references behavior changes under composite: true", () => { + describe("unittests:: config:: project-references behavior changes under composite: true", () => { it("doesn't infer the rootDir from source paths", () => { const spec: TestSpecification = { "/alpha": { @@ -308,7 +308,7 @@ namespace ts { }); }); - describe("errors when a file in a composite project occurs outside the root", () => { + describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => { it("Errors when a file is outside the rootdir", () => { const spec: TestSpecification = { "/alpha": { diff --git a/src/testRunner/unittests/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts similarity index 97% rename from src/testRunner/unittests/showConfig.ts rename to src/testRunner/unittests/config/showConfig.ts index 4040e6563a8..afe8f878d27 100644 --- a/src/testRunner/unittests/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -1,5 +1,5 @@ namespace ts { - describe("showConfig", () => { + describe("unittests:: config:: showConfig", () => { function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) { describe(name, () => { const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`; diff --git a/src/testRunner/unittests/tsconfigParsing.ts b/src/testRunner/unittests/config/tsconfigParsing.ts similarity index 97% rename from src/testRunner/unittests/tsconfigParsing.ts rename to src/testRunner/unittests/config/tsconfigParsing.ts index 255129d9fb6..638a394a34e 100644 --- a/src/testRunner/unittests/tsconfigParsing.ts +++ b/src/testRunner/unittests/config/tsconfigParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("parseConfigFileTextToJson", () => { + describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => { function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) { const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); diff --git a/src/testRunner/unittests/convertToBase64.ts b/src/testRunner/unittests/convertToBase64.ts index 56c95622977..37e38da8da7 100644 --- a/src/testRunner/unittests/convertToBase64.ts +++ b/src/testRunner/unittests/convertToBase64.ts @@ -1,5 +1,5 @@ namespace ts { - describe("convertToBase64", () => { + describe("unittests:: convertToBase64", () => { function runTest(input: string): void { const actual = convertToBase64(input); const expected = sys.base64encode!(input); diff --git a/src/testRunner/unittests/customTransforms.ts b/src/testRunner/unittests/customTransforms.ts index c4ff21d0a25..304c0a55c59 100644 --- a/src/testRunner/unittests/customTransforms.ts +++ b/src/testRunner/unittests/customTransforms.ts @@ -1,5 +1,5 @@ namespace ts { - describe("customTransforms", () => { + describe("unittests:: customTransforms", () => { function emitsCorrectly(name: string, sources: { file: string, text: string }[], customTransformers: CustomTransformers, options: CompilerOptions = {}) { it(name, () => { const roots = sources.map(source => createSourceFile(source.file, source.text, ScriptTarget.ES2015)); @@ -97,4 +97,4 @@ namespace ts { experimentalDecorators: true }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/evaluation/asyncArrow.ts b/src/testRunner/unittests/evaluation/asyncArrow.ts index 994fe8a84be..04a7af613fa 100644 --- a/src/testRunner/unittests/evaluation/asyncArrow.ts +++ b/src/testRunner/unittests/evaluation/asyncArrow.ts @@ -1,4 +1,4 @@ -describe("asyncArrowEvaluation", () => { +describe("unittests:: evaluation:: asyncArrowEvaluation", () => { // https://github.com/Microsoft/TypeScript/issues/24722 it("this capture (es5)", async () => { const result = evaluator.evaluateTypeScript(` @@ -15,4 +15,4 @@ describe("asyncArrowEvaluation", () => { await result.main(); assert.instanceOf(result.output[0].a(), result.A); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/asyncGenerator.ts b/src/testRunner/unittests/evaluation/asyncGenerator.ts index 9963ea921fa..8ca531f0759 100644 --- a/src/testRunner/unittests/evaluation/asyncGenerator.ts +++ b/src/testRunner/unittests/evaluation/asyncGenerator.ts @@ -1,4 +1,4 @@ -describe("asyncGeneratorEvaluation", () => { +describe("unittests:: evaluation:: asyncGeneratorEvaluation", () => { it("return (es5)", async () => { const result = evaluator.evaluateTypeScript(` async function * g() { @@ -27,4 +27,4 @@ describe("asyncGeneratorEvaluation", () => { { value: 0, done: true } ]); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/forAwaitOf.ts b/src/testRunner/unittests/evaluation/forAwaitOf.ts index 20ab5eed0cc..c7be018cbc9 100644 --- a/src/testRunner/unittests/evaluation/forAwaitOf.ts +++ b/src/testRunner/unittests/evaluation/forAwaitOf.ts @@ -1,4 +1,4 @@ -describe("forAwaitOfEvaluation", () => { +describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { it("sync (es5)", async () => { const result = evaluator.evaluateTypeScript(` let i = 0; diff --git a/src/testRunner/unittests/factory.ts b/src/testRunner/unittests/factory.ts index 402c399d728..cc760059c19 100644 --- a/src/testRunner/unittests/factory.ts +++ b/src/testRunner/unittests/factory.ts @@ -1,5 +1,5 @@ namespace ts { - describe("FactoryAPI", () => { + describe("unittests:: FactoryAPI", () => { function assertSyntaxKind(node: Node, expected: SyntaxKind) { assert.strictEqual(node.kind, expected, `Actual: ${Debug.showSyntaxKind(node)} Expected: ${(ts as any).SyntaxKind[expected]}`); } diff --git a/src/testRunner/unittests/incrementalParser.ts b/src/testRunner/unittests/incrementalParser.ts index fb3408b1fc3..8af44494803 100644 --- a/src/testRunner/unittests/incrementalParser.ts +++ b/src/testRunner/unittests/incrementalParser.ts @@ -120,7 +120,7 @@ namespace ts { } } - describe("Incremental", () => { + describe("unittests:: Incremental Parser", () => { it("Inserting into method", () => { const source = "class C {\r\n" + " public foo1() { }\r\n" + diff --git a/src/testRunner/unittests/jsDocParsing.ts b/src/testRunner/unittests/jsDocParsing.ts index 931785aa94d..4171a330f32 100644 --- a/src/testRunner/unittests/jsDocParsing.ts +++ b/src/testRunner/unittests/jsDocParsing.ts @@ -1,5 +1,5 @@ namespace ts { - describe("JSDocParsing", () => { + describe("unittests:: JSDocParsing", () => { describe("TypeExpressions", () => { function parsesCorrectly(name: string, content: string) { it(name, () => { diff --git a/src/testRunner/unittests/moduleResolution.ts b/src/testRunner/unittests/moduleResolution.ts index 05e17384978..69560e5449c 100644 --- a/src/testRunner/unittests/moduleResolution.ts +++ b/src/testRunner/unittests/moduleResolution.ts @@ -80,7 +80,7 @@ namespace ts { } } - describe("Node module resolution - relative paths", () => { + describe("unittests:: moduleResolution:: Node module resolution - relative paths", () => { function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { for (const ext of supportedTSExtensions) { @@ -200,7 +200,7 @@ namespace ts { }); }); - describe("Node module resolution - non-relative paths", () => { + describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => { it("computes correct commonPrefix for moduleName cache", () => { const resolutionCache = createModuleResolutionCache("/", (f) => f); let cache = resolutionCache.getOrCreateCacheForModuleName("a"); @@ -457,7 +457,7 @@ namespace ts { }); }); - describe("Module resolution - relative imports", () => { + describe("unittests:: moduleResolution:: Relative imports", () => { function test(files: Map, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { const options: CompilerOptions = { module: ModuleKind.CommonJS }; const host: CompilerHost = { @@ -530,7 +530,7 @@ export = C; }); }); - describe("Files with different casing", () => { + describe("unittests:: moduleResolution:: Files with different casing", () => { let library: SourceFile; function test(files: Map, options: CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], diagnosticCodes: number[]): void { const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -651,7 +651,7 @@ import b = require("./moduleB"); }); }); - describe("baseUrl augmented module resolution", () => { + describe("unittests:: moduleResolution:: baseUrl augmented module resolution", () => { it("module resolution without path mappings/rootDirs", () => { test(/*hasDirectoryExists*/ false); @@ -1098,7 +1098,7 @@ import b = require("./moduleB"); }); }); - describe("ModuleResolutionHost.directoryExists", () => { + describe("unittests:: moduleResolution:: ModuleResolutionHost.directoryExists", () => { it("No 'fileExists' calls if containing directory is missing", () => { const host: ModuleResolutionHost = { readFile: notImplemented, @@ -1111,7 +1111,7 @@ import b = require("./moduleB"); }); }); - describe("Type reference directive resolution: ", () => { + describe("unittests:: moduleResolution:: Type reference directive resolution: ", () => { function testWorker(hasDirectoryExists: boolean, typesRoot: string | undefined, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { const host = createModuleResolutionHost(hasDirectoryExists, ...[initialFile, targetFile].concat(...otherFiles)); const result = resolveTypeReferenceDirective(typeDirective, initialFile.name, typesRoot ? { typeRoots: [typesRoot] } : {}, host); diff --git a/src/testRunner/unittests/parsePseudoBigInt.ts b/src/testRunner/unittests/parsePseudoBigInt.ts index 0ffbee6345e..db1a841dc2b 100644 --- a/src/testRunner/unittests/parsePseudoBigInt.ts +++ b/src/testRunner/unittests/parsePseudoBigInt.ts @@ -1,5 +1,5 @@ namespace ts { - describe("BigInt literal base conversions", () => { + describe("unittests:: BigInt literal base conversions", () => { describe("parsePseudoBigInt", () => { const testNumbers: number[] = []; for (let i = 0; i < 1e3; i++) testNumbers.push(i); @@ -68,4 +68,4 @@ namespace ts { }); }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/paths.ts b/src/testRunner/unittests/paths.ts index 0b28bedc929..b1555ab2174 100644 --- a/src/testRunner/unittests/paths.ts +++ b/src/testRunner/unittests/paths.ts @@ -1,4 +1,4 @@ -describe("core paths", () => { +describe("unittests:: core paths", () => { it("normalizeSlashes", () => { assert.strictEqual(ts.normalizeSlashes("a"), "a"); assert.strictEqual(ts.normalizeSlashes("a/b"), "a/b"); @@ -289,4 +289,4 @@ describe("core paths", () => { assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); assert.strictEqual(ts.getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); }); -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/printer.ts b/src/testRunner/unittests/printer.ts index 2e69cf60876..8e217aa6a3a 100644 --- a/src/testRunner/unittests/printer.ts +++ b/src/testRunner/unittests/printer.ts @@ -1,5 +1,5 @@ namespace ts { - describe("PrinterAPI", () => { + describe("unittests:: PrinterAPI", () => { function makePrintsCorrectly(prefix: string) { return function printsCorrectly(name: string, options: PrinterOptions, printCallback: (printer: Printer) => string) { it(name, () => { diff --git a/src/testRunner/unittests/programMissingFiles.ts b/src/testRunner/unittests/programApi.ts similarity index 77% rename from src/testRunner/unittests/programMissingFiles.ts rename to src/testRunner/unittests/programApi.ts index 55dfa7f5cc2..01ef6610dba 100644 --- a/src/testRunner/unittests/programMissingFiles.ts +++ b/src/testRunner/unittests/programApi.ts @@ -11,7 +11,7 @@ namespace ts { assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); } - describe("Program.getMissingFilePaths", () => { + describe("unittests:: Program.getMissingFilePaths", () => { const options: CompilerOptions = { noLib: true, @@ -97,9 +97,37 @@ namespace ts { "d:/pretend/nonexistent4.tsx" ]); }); + + it("should not have missing file paths", () => { + const testSource = ` + class Foo extends HTMLElement { + bar: string = 'baz'; + }`; + + const host: CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => { + return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "", + writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, + getCurrentDirectory: () => sys.getCurrentDirectory(), + getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), + getNewLine: () => sys.newLine, + useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, + fileExists: fileName => fileName === "test.ts", + readFile: fileName => fileName === "test.ts" ? testSource : undefined, + resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); }, + getDirectories: _path => { throw new Error("unsupported"); }, + }; + + const program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host); + assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1"); + assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0"); + assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); + }); }); - describe("Program.isSourceFileFromExternalLibrary", () => { + describe("unittests:: Program.isSourceFileFromExternalLibrary", () => { it("works on redirect files", () => { // In this example '/node_modules/foo/index.d.ts' will redirect to '/node_modules/bar/node_modules/foo/index.d.ts'. const a = new documents.TextDocument("/a.ts", 'import * as bar from "bar"; import * as foo from "foo";'); diff --git a/src/testRunner/unittests/programNoParseFalsyFileNames.ts b/src/testRunner/unittests/programNoParseFalsyFileNames.ts deleted file mode 100644 index 8040e7c7f43..00000000000 --- a/src/testRunner/unittests/programNoParseFalsyFileNames.ts +++ /dev/null @@ -1,36 +0,0 @@ -namespace ts { - describe("programNoParseFalsyFileNames", () => { - let program: Program; - - beforeEach(() => { - const testSource = ` - class Foo extends HTMLElement { - bar: string = 'baz'; - }`; - - const host: CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => { - return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "", - writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, - getCurrentDirectory: () => sys.getCurrentDirectory(), - getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), - getNewLine: () => sys.newLine, - useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, - fileExists: fileName => fileName === "test.ts", - readFile: fileName => fileName === "test.ts" ? testSource : undefined, - resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); }, - getDirectories: _path => { throw new Error("unsupported"); }, - }; - - program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host); - }); - - it("should not have missing file paths", () => { - assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1"); - assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0"); - assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); - }); - }); -} \ No newline at end of file diff --git a/src/testRunner/unittests/projectErrors.ts b/src/testRunner/unittests/projectErrors.ts deleted file mode 100644 index b17494b59fb..00000000000 --- a/src/testRunner/unittests/projectErrors.ts +++ /dev/null @@ -1,202 +0,0 @@ -namespace ts.projectSystem { - describe("Project errors", () => { - function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: ReadonlyArray): void { - assert.isTrue(projectFiles !== undefined, "missing project files"); - checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); - } - - function checkProjectErrorsWorker(errors: ReadonlyArray, expectedErrors: ReadonlyArray): void { - assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); - if (expectedErrors.length) { - for (let i = 0; i < errors.length; i++) { - const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n"); - const expectedMessage = expectedErrors[i]; - assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); - } - } - } - - function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { - assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); - if (expectedErrors.length) { - zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { - assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); - }); - } - } - - it("external project - diagnostics for missing files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/applib.ts", - content: "" - }; - const host = createServerHost([file1, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/test.csproj"; - const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }; - - { - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: toExternalFiles([file1.path, file2.path]) - }); - - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - // only file1 exists - expect error - checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - } - host.reloadFS([file2, libFile]); - { - // only file2 exists - expect error - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); - } - - host.reloadFS([file1, file2, libFile]); - { - // both files exist - expect no errors - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, []); - } - }); - - it("configured projects - diagnostics for missing files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/applib.ts", - content: "" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const host = createServerHost([file1, config, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - openFilesForSession([file1], session); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = configuredProjectAt(projectService, 0); - const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: project.getProjectName() } - }; - let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - - host.reloadFS([file1, file2, config, libFile]); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, []); - }); - - it("configured projects - diagnostics for corrupted config 1", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/lib.ts", - content: "" - }; - const correctConfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = createServerHost([file1, file2, corruptedConfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); - } - // fix config and trigger watcher - host.reloadFS([file1, file2, correctConfig]); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } - }); - - it("configured projects - diagnostics for corrupted config 2", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/lib.ts", - content: "" - }; - const correctConfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = createServerHost([file1, file2, correctConfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } - // break config and trigger watcher - host.reloadFS([file1, file2, corruptedConfig]); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); - } - }); - }); -} diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index 9e56b48a911..c9ea0e77fec 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -210,7 +210,7 @@ namespace ts { checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); } - describe("Reuse program structure", () => { + describe("unittests:: Reuse program structure:: General", () => { const target = ScriptTarget.Latest; const files: NamedSourceText[] = [ { @@ -895,7 +895,7 @@ namespace ts { }); }); - describe("host is optional", () => { + describe("unittests:: Reuse program structure:: host is optional", () => { it("should work if host is not provided", () => { createProgram([], {}); }); @@ -905,7 +905,7 @@ namespace ts { import createTestSystem = TestFSWithWatch.createWatchedSystem; import libFile = TestFSWithWatch.libFile; - describe("isProgramUptoDate should return true when there is no change in compiler options and", () => { + describe("unittests:: Reuse program structure:: isProgramUptoDate should return true when there is no change in compiler options and", () => { function verifyProgramIsUptoDate( program: Program, newRootFileNames: string[], diff --git a/src/testRunner/unittests/semver.ts b/src/testRunner/unittests/semver.ts index 357a24307ca..079bdc3d512 100644 --- a/src/testRunner/unittests/semver.ts +++ b/src/testRunner/unittests/semver.ts @@ -1,6 +1,6 @@ namespace ts { import theory = utils.theory; - describe("semver", () => { + describe("unittests:: semver", () => { describe("Version", () => { function assertVersion(version: Version, [major, minor, patch, prerelease, build]: [number, number, number, string[]?, string[]?]) { assert.strictEqual(version.major, major); @@ -225,4 +225,4 @@ namespace ts { ]); }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts similarity index 96% rename from src/testRunner/unittests/cancellableLanguageServiceOperations.ts rename to src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts index 37f829d67a7..9868d628d0c 100644 --- a/src/testRunner/unittests/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts @@ -1,5 +1,5 @@ namespace ts { - describe("cancellableLanguageServiceOperations", () => { + describe("unittests:: services:: cancellableLanguageServiceOperations", () => { const file = ` function foo(): void; function foo(x: T): T; diff --git a/src/testRunner/unittests/services/colorization.ts b/src/testRunner/unittests/services/colorization.ts index e3295e6fee8..71ba8bad394 100644 --- a/src/testRunner/unittests/services/colorization.ts +++ b/src/testRunner/unittests/services/colorization.ts @@ -6,7 +6,7 @@ interface ClassificationEntry { position?: number; } -describe("Colorization", () => { +describe("unittests:: services:: Colorization", () => { // Use the shim adapter to ensure test coverage of the shim layer for the classifier const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false); const classifier = languageServiceAdapter.getClassifier(); diff --git a/src/testRunner/unittests/convertToAsyncFunction.ts b/src/testRunner/unittests/services/convertToAsyncFunction.ts similarity index 95% rename from src/testRunner/unittests/convertToAsyncFunction.ts rename to src/testRunner/unittests/services/convertToAsyncFunction.ts index 21c6a29f8e4..8c426effa4c 100644 --- a/src/testRunner/unittests/convertToAsyncFunction.ts +++ b/src/testRunner/unittests/services/convertToAsyncFunction.ts @@ -255,7 +255,7 @@ interface String { charAt: any; } interface Array {}` }; - function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false) { + function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false, onlyProvideAction = false) { const t = extractTest(text); const selectionRange = t.ranges.get("selection")!; if (!selectionRange) { @@ -307,7 +307,7 @@ interface Array {}` const actions = codefix.getFixes(context); const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message); - if (expectFailure) { + if (expectFailure && !onlyProvideAction) { assert.isNotTrue(action && action.changes.length > 0); return; } @@ -343,7 +343,7 @@ interface Array {}` } } - describe("convertToAsyncFunctions", () => { + describe("unittests:: services:: convertToAsyncFunctions", () => { _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(result => { console.log(result) }); @@ -1151,25 +1151,25 @@ function [#|f|]() { _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpression", ` const [#|foo|] = function () { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); -} +} `); _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionWithName", ` const foo = function [#|f|]() { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); -} +} `); _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionAssignedToBindingPattern", ` const { length } = [#|function|] () { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); -} +} `); _testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParams", ` function [#|f|]() { - return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x); -} + return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x); +} `); _testConvertToAsyncFunction("convertToAsyncFunction_bindingPattern", ` @@ -1178,7 +1178,7 @@ function [#|f|]() { } function res({ status, trailer }){ console.log(status); -} +} `); _testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", ` @@ -1188,7 +1188,7 @@ function [#|f|]() { } function res({ status, trailer }){ console.log(status); -} +} `); _testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunction", ` @@ -1209,7 +1209,7 @@ function [#|f|]() { } function res(result) { return Promise.resolve().then(x => console.log(result)); -} +} `); _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromise", ` @@ -1241,7 +1241,7 @@ function [#|f|]() { return Promise.resolve(1) .then(x => Promise.reject(x)) .catch(err => console.log(err)); -} +} `); _testConvertToAsyncFunction("convertToAsyncFunction_nestedPromises", ` @@ -1266,6 +1266,22 @@ _testConvertToAsyncFunction("convertToAsyncFunction_exportModifier", ` export function [#|foo|]() { return fetch('https://typescriptlang.org').then(s => console.log(s)); } +`); + +_testConvertToAsyncFunction("convertToAsyncFunction_OutermostOnlySuccess", ` +function [#|foo|]() { + return fetch('a').then(() => { + return fetch('b').then(() => 'c'); + }) +} +`); + +_testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", ` +function foo() { + return fetch('a').then([#|() => {|] + return fetch('b').then(() => 'c'); + }) +} `); }); @@ -1276,4 +1292,8 @@ export function [#|foo|]() { function _testConvertToAsyncFunctionFailed(caption: string, text: string) { testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true); } + + function _testConvertToAsyncFunctionFailedSuggestion(caption: string, text: string) { + testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true, /*onlyProvideAction*/ true); + } } diff --git a/src/testRunner/unittests/services/documentRegistry.ts b/src/testRunner/unittests/services/documentRegistry.ts index a3dad56f42b..e7c80486e97 100644 --- a/src/testRunner/unittests/services/documentRegistry.ts +++ b/src/testRunner/unittests/services/documentRegistry.ts @@ -1,4 +1,4 @@ -describe("DocumentRegistry", () => { +describe("unittests:: services:: DocumentRegistry", () => { it("documents are shared between projects", () => { const documentRegistry = ts.createDocumentRegistry(); const defaultCompilerOptions = ts.getDefaultCompilerOptions(); diff --git a/src/testRunner/unittests/extractConstants.ts b/src/testRunner/unittests/services/extract/constants.ts similarity index 94% rename from src/testRunner/unittests/extractConstants.ts rename to src/testRunner/unittests/services/extract/constants.ts index e0ef305812c..d9cd5010d5b 100644 --- a/src/testRunner/unittests/extractConstants.ts +++ b/src/testRunner/unittests/services/extract/constants.ts @@ -1,5 +1,5 @@ namespace ts { - describe("extractConstants", () => { + describe("unittests:: services:: extract:: extractConstants", () => { testExtractConstant("extractConstant_TopLevel", `let x = [#|1|];`); diff --git a/src/testRunner/unittests/extractFunctions.ts b/src/testRunner/unittests/services/extract/functions.ts similarity index 95% rename from src/testRunner/unittests/extractFunctions.ts rename to src/testRunner/unittests/services/extract/functions.ts index 1f90e1cc600..d872ad81a63 100644 --- a/src/testRunner/unittests/extractFunctions.ts +++ b/src/testRunner/unittests/services/extract/functions.ts @@ -1,5 +1,5 @@ namespace ts { - describe("extractFunctions", () => { + describe("unittests:: services:: extract:: extractFunctions", () => { testExtractFunction("extractFunction1", `namespace A { let x = 1; diff --git a/src/testRunner/unittests/extractTestHelpers.ts b/src/testRunner/unittests/services/extract/helpers.ts similarity index 100% rename from src/testRunner/unittests/extractTestHelpers.ts rename to src/testRunner/unittests/services/extract/helpers.ts diff --git a/src/testRunner/unittests/extractRanges.ts b/src/testRunner/unittests/services/extract/ranges.ts similarity index 95% rename from src/testRunner/unittests/extractRanges.ts rename to src/testRunner/unittests/services/extract/ranges.ts index 9cd76dd49e9..265dd33e60a 100644 --- a/src/testRunner/unittests/extractRanges.ts +++ b/src/testRunner/unittests/services/extract/ranges.ts @@ -42,7 +42,7 @@ namespace ts { } } - describe("extractRanges", () => { + describe("unittests:: services:: extract:: extractRanges", () => { it("get extract range from selection", () => { testExtractRange(` [#| @@ -418,4 +418,4 @@ switch (x) { testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.cannotExtractIdentifier.message]); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/symbolWalker.ts b/src/testRunner/unittests/services/extract/symbolWalker.ts similarity index 93% rename from src/testRunner/unittests/symbolWalker.ts rename to src/testRunner/unittests/services/extract/symbolWalker.ts index 4743b87133b..58d9dcb577f 100644 --- a/src/testRunner/unittests/symbolWalker.ts +++ b/src/testRunner/unittests/services/extract/symbolWalker.ts @@ -1,5 +1,5 @@ namespace ts { - describe("Symbol Walker", () => { + describe("unittests:: services:: extract:: Symbol Walker", () => { function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) { it(description, () => { const result = Harness.Compiler.compileFiles([{ @@ -42,4 +42,4 @@ export default function foo(a: number, b: Bar): void {}`, (file, checker) => { assert.equal(stdLibRefSymbols, 1); // Expect 1 stdlib entry symbol - the implicit Array referenced by Bar.history }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/hostNewLineSupport.ts b/src/testRunner/unittests/services/hostNewLineSupport.ts similarity index 95% rename from src/testRunner/unittests/hostNewLineSupport.ts rename to src/testRunner/unittests/services/hostNewLineSupport.ts index abd79210086..cafe4813431 100644 --- a/src/testRunner/unittests/hostNewLineSupport.ts +++ b/src/testRunner/unittests/services/hostNewLineSupport.ts @@ -1,5 +1,5 @@ namespace ts { - describe("hostNewLineSupport", () => { + describe("unittests:: services:: hostNewLineSupport", () => { function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) { function snapFor(path: string): IScriptSnapshot | undefined { if (path === "lib.d.ts") { @@ -46,4 +46,4 @@ namespace ts { `); }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/languageService.ts b/src/testRunner/unittests/services/languageService.ts similarity index 94% rename from src/testRunner/unittests/languageService.ts rename to src/testRunner/unittests/services/languageService.ts index ce8fa93d1f9..d646f32383f 100644 --- a/src/testRunner/unittests/languageService.ts +++ b/src/testRunner/unittests/services/languageService.ts @@ -1,5 +1,5 @@ namespace ts { - describe("languageService", () => { + describe("unittests:: services:: languageService", () => { const files: {[index: string]: string} = { "foo.ts": `import Vue from "./vue"; import Component from "./vue-class-component"; @@ -43,4 +43,4 @@ export function Component(x: Config): any;` expect(definitions).to.exist; // tslint:disable-line no-unused-expression }); }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts similarity index 95% rename from src/testRunner/unittests/organizeImports.ts rename to src/testRunner/unittests/services/organizeImports.ts index 355292c6d89..d3fe55f9686 100644 --- a/src/testRunner/unittests/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1,5 +1,5 @@ namespace ts { - describe("Organize imports", () => { + describe("unittests:: services:: Organize imports", () => { describe("Sort imports", () => { it("Sort - non-relative vs non-relative", () => { assertSortsBefore( @@ -274,6 +274,16 @@ export const Other = 1; assert.isEmpty(changes); }); + it("doesn't crash on shorthand ambient module", () => { + const testFile = { + path: "/a.ts", + content: "declare module '*';", + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatSettings, emptyOptions); + assert.isEmpty(changes); + }); + testOrganizeImports("Renamed_used", { path: "/test.ts", diff --git a/src/testRunner/unittests/services/patternMatcher.ts b/src/testRunner/unittests/services/patternMatcher.ts index 5e35d2020db..9f3cbf4b6d7 100644 --- a/src/testRunner/unittests/services/patternMatcher.ts +++ b/src/testRunner/unittests/services/patternMatcher.ts @@ -1,4 +1,4 @@ -describe("PatternMatcher", () => { +describe("unittests:: services:: PatternMatcher", () => { describe("BreakIntoCharacterSpans", () => { it("EmptyIdentifier", () => { verifyBreakIntoCharacterSpans(""); diff --git a/src/testRunner/unittests/services/preProcessFile.ts b/src/testRunner/unittests/services/preProcessFile.ts index a89b6337c84..3e9d16672db 100644 --- a/src/testRunner/unittests/services/preProcessFile.ts +++ b/src/testRunner/unittests/services/preProcessFile.ts @@ -1,4 +1,4 @@ -describe("PreProcessFile:", () => { +describe("unittests:: services:: PreProcessFile:", () => { function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); diff --git a/src/testRunner/unittests/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts similarity index 97% rename from src/testRunner/unittests/textChanges.ts rename to src/testRunner/unittests/services/textChanges.ts index 164073207b3..de92efafbeb 100644 --- a/src/testRunner/unittests/textChanges.ts +++ b/src/testRunner/unittests/services/textChanges.ts @@ -2,7 +2,7 @@ // tslint:disable trim-trailing-whitespace namespace ts { - describe("textChanges", () => { + describe("unittests:: services:: textChanges", () => { function findChild(name: string, n: Node) { return find(n)!; @@ -753,4 +753,4 @@ let x = foo }); } }); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/transpile.ts b/src/testRunner/unittests/services/transpile.ts similarity index 97% rename from src/testRunner/unittests/transpile.ts rename to src/testRunner/unittests/services/transpile.ts index b545b76c3db..d197d6db621 100644 --- a/src/testRunner/unittests/transpile.ts +++ b/src/testRunner/unittests/services/transpile.ts @@ -1,5 +1,5 @@ namespace ts { - describe("Transpile", () => { + describe("unittests:: services:: Transpile", () => { interface TranspileTestSettings { options?: TranspileOptions; diff --git a/src/testRunner/unittests/transform.ts b/src/testRunner/unittests/transform.ts index f219fb6c683..98c29025d5f 100644 --- a/src/testRunner/unittests/transform.ts +++ b/src/testRunner/unittests/transform.ts @@ -1,5 +1,5 @@ namespace ts { - describe("TransformAPI", () => { + describe("unittests:: TransformAPI", () => { function replaceUndefinedWithVoid0(context: TransformationContext) { const previousOnSubstituteNode = context.onSubstituteNode; context.enableSubstitution(SyntaxKind.Identifier); diff --git a/src/testRunner/unittests/tsbuild.ts b/src/testRunner/unittests/tsbuild.ts index 72b1121e861..bf628e10598 100644 --- a/src/testRunner/unittests/tsbuild.ts +++ b/src/testRunner/unittests/tsbuild.ts @@ -8,7 +8,7 @@ namespace ts { "/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; - describe("tsbuild - sanity check of clean build of 'sample1' project", () => { + describe("unittests:: tsbuild - sanity check of clean build of 'sample1' project", () => { it("can build the sample project 'sample1' without error", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -61,7 +61,7 @@ namespace ts { }); }); - describe("tsbuild - dry builds", () => { + describe("unittests:: tsbuild - dry builds", () => { it("doesn't write any files in a dry build", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -90,7 +90,7 @@ namespace ts { }); }); - describe("tsbuild - clean builds", () => { + describe("unittests:: tsbuild - clean builds", () => { it("removes all files it built", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -111,7 +111,7 @@ namespace ts { }); }); - describe("tsbuild - force builds", () => { + describe("unittests:: tsbuild - force builds", () => { it("always builds under --force", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -137,7 +137,7 @@ namespace ts { }); }); - describe("tsbuild - can detect when and what to rebuild", () => { + describe("unittests:: tsbuild - can detect when and what to rebuild", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); @@ -200,7 +200,7 @@ namespace ts { }); }); - describe("tsbuild - downstream-blocked compilations", () => { + describe("unittests:: tsbuild - downstream-blocked compilations", () => { it("won't build downstream projects if upstream projects have errors", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -222,7 +222,7 @@ namespace ts { }); }); - describe("tsbuild - project invalidation", () => { + describe("unittests:: tsbuild - project invalidation", () => { it("invalidates projects correctly", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -270,7 +270,7 @@ export class cNew {}`); }); }); - describe("tsbuild - with resolveJsonModule option", () => { + describe("unittests:: tsbuild - with resolveJsonModule option", () => { const projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); const allExpectedOutputs = ["/src/tests/dist/src/index.js", "/src/tests/dist/src/index.d.ts", "/src/tests/dist/src/hello.json"]; @@ -320,7 +320,7 @@ export default hello.hello`); }); }); - describe("tsbuild - lists files", () => { + describe("unittests:: tsbuild - lists files", () => { it("listFiles", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -369,7 +369,7 @@ export default hello.hello`); }); }); - describe("tsbuild - with rootDir of project reference in parentDirectory", () => { + describe("unittests:: tsbuild - with rootDir of project reference in parentDirectory", () => { const projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); const allExpectedOutputs = [ "/src/dist/other/other.js", "/src/dist/other/other.d.ts", @@ -388,7 +388,7 @@ export default hello.hello`); }); }); - describe("tsbuild - when project reference is referenced transitively", () => { + describe("unittests:: tsbuild - when project reference is referenced transitively", () => { const projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); const allExpectedOutputs = [ "/src/a.js", "/src/a.d.ts", @@ -460,7 +460,7 @@ export const b = new A();`); export namespace OutFile { const outFileFs = loadProjectFromDisk("tests/projects/outfile-concat"); - describe("tsbuild - baseline sectioned sourcemaps", () => { + describe("unittests:: tsbuild - baseline sectioned sourcemaps", () => { let fs: vfs.FileSystem | undefined; before(() => { fs = outFileFs.shadow(); @@ -480,7 +480,7 @@ export const b = new A();`); }); }); - describe("tsbuild - downstream prepend projects always get rebuilt", () => { + describe("unittests:: tsbuild - downstream prepend projects always get rebuilt", () => { it("", () => { const fs = outFileFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -508,7 +508,7 @@ export const b = new A();`); "/src/core/index.d.ts.map", ]; - describe("tsbuild - empty files option in tsconfig", () => { + describe("unittests:: tsbuild - empty files option in tsconfig", () => { it("has empty files diagnostic when files is empty and no references are provided", () => { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); @@ -541,7 +541,7 @@ export const b = new A();`); }); } - describe("tsbuild - graph-ordering", () => { + describe("unittests:: tsbuild - graph-ordering", () => { let host: fakes.SolutionBuilderHost | undefined; const deps: [string, string][] = [ ["A", "B"], diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 9178416373c..c1ddb0aa4e7 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -1,5 +1,4 @@ namespace ts.tscWatch { - export import libFile = TestFSWithWatch.libFile; import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation; import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath; import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; @@ -15,7 +14,7 @@ namespace ts.tscWatch { return solutionBuilder; } - describe("tsbuild-watch program updates", () => { + describe("unittests:: tsbuild-watch program updates", () => { const project = "sample1"; const enum SubProject { core = "core", @@ -480,7 +479,7 @@ let x: string = 10;`); const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); // Build in watch mode - const watch = createWatchOfConfigFileReturningBuilder(watchConfig, host); + const watch = createWatchOfConfigFile(watchConfig, host); checkOutputErrorsInitial(host, emptyArray); return { host, solutionBuilder, watch }; @@ -506,10 +505,8 @@ let x: string = 10;`); projectSystem.checkProjectActualFiles(service.configuredProjects.get(configFile.toLowerCase())!, expectedFiles); } - type Watch = () => BuilderProgram; - function verifyDependencies(watch: Watch, filePath: string, expected: ReadonlyArray) { - checkArray(`${filePath} dependencies`, watch().getAllDependencies(watch().getSourceFile(filePath)!), expected); + checkArray(`${filePath} dependencies`, watch.getBuilderProgram().getAllDependencies(watch().getSourceFile(filePath)!), expected); } describe("on sample project", () => { @@ -543,7 +540,7 @@ let x: string = 10;`); host.checkTimeoutQueueLengthAndRun(1); checkOutputErrorsIncremental(host, emptyArray); - checkProgramActualFiles(watch().getProgram(), expectedFilesAfterEdit); + checkProgramActualFiles(watch(), expectedFilesAfterEdit); }); @@ -643,7 +640,7 @@ export function gfoo() { expectedWatchedDirectoriesRecursive: ReadonlyArray, dependencies: ReadonlyArray<[string, ReadonlyArray]>, expectedWatchedDirectories?: ReadonlyArray) { - checkProgramActualFiles(watch().getProgram(), expectedProgramFiles); + checkProgramActualFiles(watch(), expectedProgramFiles); verifyWatchesOfProject(host, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); for (const [file, deps] of dependencies) { verifyDependencies(watch, file, deps); diff --git a/src/testRunner/unittests/tscWatch/consoleClearing.ts b/src/testRunner/unittests/tscWatch/consoleClearing.ts new file mode 100644 index 00000000000..d0669a2327d --- /dev/null +++ b/src/testRunner/unittests/tscWatch/consoleClearing.ts @@ -0,0 +1,97 @@ +namespace ts.tscWatch { + describe("unittests:: tsc-watch:: console clearing", () => { + const currentDirectoryLog = "Current directory: / CaseSensitiveFileNames: false\n"; + const fileWatcherAddedLog = [ + "FileWatcher:: Added:: WatchInfo: /f.ts 250 Source file\n", + "FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 Source file\n" + ]; + + const file: File = { + path: "/f.ts", + content: "" + }; + + function getProgramSynchronizingLog(options: CompilerOptions) { + return [ + "Synchronizing program\n", + "CreatingProgramWith::\n", + " roots: [\"/f.ts\"]\n", + ` options: ${JSON.stringify(options)}\n` + ]; + } + + function isConsoleClearDisabled(options: CompilerOptions) { + return options.diagnostics || options.extendedDiagnostics || options.preserveWatchOutput; + } + + function verifyCompilation(host: WatchedSystem, options: CompilerOptions, initialDisableOptions?: CompilerOptions) { + const disableConsoleClear = isConsoleClearDisabled(options); + const hasLog = options.extendedDiagnostics || options.diagnostics; + checkOutputErrorsInitial(host, emptyArray, initialDisableOptions ? isConsoleClearDisabled(initialDisableOptions) : disableConsoleClear, hasLog ? [ + currentDirectoryLog, + ...getProgramSynchronizingLog(options), + ...(options.extendedDiagnostics ? fileWatcherAddedLog : emptyArray) + ] : undefined); + host.modifyFile(file.path, "//"); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray, disableConsoleClear, hasLog ? [ + "FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n", + "Scheduling update\n", + "Elapsed:: 0ms FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n" + ] : undefined, hasLog ? getProgramSynchronizingLog(options) : undefined); + } + + function checkConsoleClearingUsingCommandLineOptions(options: CompilerOptions = {}) { + const files = [file, libFile]; + const host = createWatchedSystem(files); + createWatchOfFilesAndCompilerOptions([file.path], host, options); + verifyCompilation(host, options); + } + + it("without --diagnostics or --extendedDiagnostics", () => { + checkConsoleClearingUsingCommandLineOptions(); + }); + it("with --diagnostics", () => { + checkConsoleClearingUsingCommandLineOptions({ + diagnostics: true, + }); + }); + it("with --extendedDiagnostics", () => { + checkConsoleClearingUsingCommandLineOptions({ + extendedDiagnostics: true, + }); + }); + it("with --preserveWatchOutput", () => { + checkConsoleClearingUsingCommandLineOptions({ + preserveWatchOutput: true, + }); + }); + + describe("when preserveWatchOutput is true in config file", () => { + const compilerOptions: CompilerOptions = { + preserveWatchOutput: true + }; + const configFile: File = { + path: "/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + const files = [file, configFile, libFile]; + it("using createWatchOfConfigFile ", () => { + const host = createWatchedSystem(files); + createWatchOfConfigFile(configFile.path, host); + // Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed + verifyCompilation(host, compilerOptions, {}); + }); + it("when createWatchProgram is invoked with configFileParseResult on WatchCompilerHostOfConfigFile", () => { + const host = createWatchedSystem(files); + const reportDiagnostic = createDiagnosticReporter(host); + const optionsToExtend: CompilerOptions = {}; + const configParseResult = parseConfigFileWithSystem(configFile.path, optionsToExtend, host, reportDiagnostic)!; + const watchCompilerHost = createWatchCompilerHostOfConfigFile(configParseResult.options.configFilePath!, optionsToExtend, host, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(host)); + watchCompilerHost.configFileParsingResult = configParseResult; + createWatchProgram(watchCompilerHost); + verifyCompilation(host, compilerOptions); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tscWatch/emit.ts b/src/testRunner/unittests/tscWatch/emit.ts new file mode 100644 index 00000000000..06aec75f31f --- /dev/null +++ b/src/testRunner/unittests/tscWatch/emit.ts @@ -0,0 +1,718 @@ +namespace ts.tscWatch { + function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { + return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; + } + + function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + return `TSFILE: ${filename}${host.newLine}`; + } + + interface FileOrFolderEmit extends File { + output?: string; + } + + function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { + const result = file as FileOrFolderEmit; + if (getOutput) { + result.output = getOutput(file); + } + return result; + } + + function getEmittedLines(files: FileOrFolderEmit[]) { + const seen = createMap(); + const result: string[] = []; + for (const { output } of files) { + if (output && !seen.has(output)) { + seen.set(output, true); + result.push(output); + } + } + return result; + } + + function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { + const expectedAffectedFiles = getEmittedLines(affectedFiles); + const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); + checkOutputContains(host, expectedAffectedFiles); + checkOutputDoesNotContain(host, expectedNonAffectedFiles); + } + + describe("unittests:: tsc-watch:: emit with outFile or out setting", () => { + function createWatchForOut(out?: string, outFile?: string) { + const host = createWatchedSystem([]); + const config: FileOrFolderEmit = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true } + }) + }; + + let getOutput: (file: File) => string; + if (out) { + config.content = JSON.stringify({ + compilerOptions: { listEmittedFiles: true, out } + }); + getOutput = __ => getEmittedLineForSingleFileOutput(out, host); + } + else if (outFile) { + config.content = JSON.stringify({ + compilerOptions: { listEmittedFiles: true, outFile } + }); + getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host); + } + else { + getOutput = file => getEmittedLineForMultiFileOutput(file, host); + } + + const f1 = getFileOrFolderEmit({ + path: "/a/a.ts", + content: "let x = 1" + }, getOutput); + const f2 = getFileOrFolderEmit({ + path: "/a/b.ts", + content: "let y = 1" + }, getOutput); + + const files = [f1, f2, config, libFile]; + host.reloadFS(files); + createWatchOfConfigFile(config.path, host); + + const allEmittedLines = getEmittedLines(files); + checkOutputContains(host, allEmittedLines); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkAffectedLines(host, [f1], allEmittedLines); + } + + it("projectUsesOutFile should not be returned if not set", () => { + createWatchForOut(); + }); + + it("projectUsesOutFile should be true if out is set", () => { + const outJs = "/a/out.js"; + createWatchForOut(outJs); + }); + + it("projectUsesOutFile should be true if outFile is set", () => { + const outJs = "/a/out.js"; + createWatchForOut(/*out*/ undefined, outJs); + }); + + function verifyFilesEmittedOnce(useOutFile: boolean) { + const file1: File = { + path: "/a/b/output/AnotherDependency/file1.d.ts", + content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" + }; + const file2: File = { + path: "/a/b/dependencies/file2.d.ts", + content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" + }; + const file3: File = { + path: "/a/b/project/src/main.ts", + content: "namespace Main { export function fooBar() {} }" + }; + const file4: File = { + path: "/a/b/project/src/main2.ts", + content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" + }; + const configFile: File = { + path: "/a/b/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: useOutFile ? + { outFile: "../output/common.js", target: "es5" } : + { outDir: "../output", target: "es5" }, + files: [file1.path, file2.path, file3.path, file4.path] + }) + }; + const files = [file1, file2, file3, file4]; + const allfiles = files.concat(configFile); + const host = createWatchedSystem(allfiles); + const originalWriteFile = host.writeFile.bind(host); + const mapOfFilesWritten = createMap(); + host.writeFile = (p: string, content: string) => { + const count = mapOfFilesWritten.get(p); + mapOfFilesWritten.set(p, count ? count + 1 : 1); + return originalWriteFile(p, content); + }; + createWatchOfConfigFile(configFile.path, host); + if (useOutFile) { + // Only out file + assert.equal(mapOfFilesWritten.size, 1); + } + else { + // main.js and main2.js + assert.equal(mapOfFilesWritten.size, 2); + } + mapOfFilesWritten.forEach((value, key) => { + assert.equal(value, 1, "Key: " + key); + }); + } + + it("with --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ true); + }); + + it("without --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ false); + }); + }); + + describe("unittests:: tsc-watch:: emit for configured projects", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const configFilePath = "/a/b/tsconfig.json"; + interface InitialStateParams { + /** custom config file options */ + configObj?: any; + /** list of the files that will be emitted for first compilation */ + firstCompilationEmitFiles?: string[]; + /** get the emit file for file - default is multi file emit line */ + getEmitLine?(file: File, host: WatchedSystem): string; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): File[]; + /** initial list of files to emit if not the default list */ + firstReloadFileList?: string[]; + } + function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { + const host = createWatchedSystem([]); + const getOutputName = getEmitLine ? (file: File) => getEmitLine(file, host) : + (file: File) => getEmittedLineForMultiFileOutput(file, host); + + const moduleFile1 = getFileOrFolderEmit({ + path: moduleFile1Path, + content: "export function Foo() { };", + }, getOutputName); + + const file1Consumer1 = getFileOrFolderEmit({ + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }, getOutputName); + + const file1Consumer2 = getFileOrFolderEmit({ + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }, getOutputName); + + const moduleFile2 = getFileOrFolderEmit({ + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }, getOutputName); + + const globalFile3 = getFileOrFolderEmit({ + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }); + + const additionalFiles = getAdditionalFileOrFolder ? + map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) : + []; + + (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; + const configFile = getFileOrFolderEmit({ + path: configFilePath, + content: JSON.stringify(configObj) + }); + + const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; + let allEmittedFiles = getEmittedLines(files); + host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); + + // Initial compile + createWatchOfConfigFile(configFile.path, host); + if (firstCompilationEmitFiles) { + checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); + } + else { + checkOutputContains(host, allEmittedFiles); + } + host.clearOutput(); + + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + getFile, + verifyAffectedFiles, + verifyAffectedAllFiles, + getOutputName + }; + + function getFiles(filelist: string[]) { + return map(filelist, getFile); + } + + function getFile(fileName: string) { + return find(files, file => file.path === fileName)!; + } + + function verifyAffectedAllFiles() { + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputContains(host, allEmittedFiles); + host.clearOutput(); + } + + function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) { + if (!filesToReload) { + filesToReload = files; + } + else if (filesToReload.length > files.length) { + allEmittedFiles = getEmittedLines(filesToReload); + } + host.reloadFS(filesToReload); + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedLines(host, expected, allEmittedFiles); + host.clearOutput(); + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should be up-to-date with the reference map changes", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); + + // Change file1Consumer1 content to `export let y = Foo();` + file1Consumer1.content = `export let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer2]); + + // Add the import statements back to file1Consumer1 + file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]); + + // Multiple file edits in one go: + + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + file1Consumer1.content = `export let y = Foo();`; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + }); + + it("should be up-to-date with deleted files", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + files, + verifyAffectedFiles + } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + + // Delete file1Consumer2 + const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file); + verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad); + }); + + it("should be up-to-date with newly created files", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + files, + verifyAffectedFiles, + getOutputName + } = getInitialState(); + + const file1Consumer3 = getFileOrFolderEmit({ + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }, getOutputName); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); + }); + + it("should detect changes in non-root files", () => { + const { + moduleFile1, file1Consumer1, + verifyAffectedFiles + } = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1]); + + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyAffectedFiles([moduleFile1]); + }); + + it("should return all files if a global file changed shape", () => { + const { + globalFile3, verifyAffectedAllFiles + } = getInitialState(); + + globalFile3.content += "var T2: string;"; + verifyAffectedAllFiles(); + }); + + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { + moduleFile1, verifyAffectedFiles + } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { + moduleFile1, verifyAffectedFiles + } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } }, + getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host) + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { + moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile + } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] + }); + + const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); + file1Consumer1.content += "export var T: number;"; + verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); + + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change both files before the timeout + file1Consumer1.content += "export var T2: number;"; + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]); + }); + + it("should work fine for files with circular references", () => { + // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode + + const file1: File = { + path: "/a/b/file1.ts", + content: ` + /// + export var t1 = 10;` + }; + const file2: File = { + path: "/a/b/file2.ts", + content: ` + /// + export var t2 = 10;` + }; + const { + configFile, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [file1.path, file2.path], + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath] + }); + const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path); + + file1Emit.content += "export var t3 = 10;"; + verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]); + + }); + + it("should detect removed code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { + configFile, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path], + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath] + }); + + const referenceFile1Emit = getFile(referenceFile1.path); + verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { + configFile, + moduleFile2, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [referenceFile1.path], + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath] + }); + + const referenceFile1Emit = getFile(referenceFile1.path); + referenceFile1Emit.content += "export var yy = Foo();"; + verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); + + // Create module File2 and see both files are saved + verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); + }); + }); + + describe("unittests:: tsc-watch:: emit file content", () => { + interface EmittedFile extends File { + shouldBeWritten: boolean; + } + function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { + return map(contents, (content, index) => { + return { + content, + path: changeExtension(files[index].path, Extension.Js), + shouldBeWritten: true + }; + } + ); + } + function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { + for (const { path, content, shouldBeWritten } of emittedFiles) { + if (shouldBeWritten) { + assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`); + assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`); + } + else { + assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); + } + } + } + + function verifyEmittedFileContents(newLine: string, inputFiles: File[], initialEmittedFileContents: string[], + modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: File) { + const host = createWatchedSystem([], { newLine }); + const files = concatenate( + map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))), + configFile ? [libFile, configFile] : [libFile] + ); + const allEmittedFiles = getEmittedLines(files); + host.reloadFS(files); + + // Initial compile + if (configFile) { + createWatchOfConfigFile(configFile.path, host); + } + else { + // First file as the root + createWatchOfFilesAndCompilerOptions([files[0].path], host, { listEmittedFiles: true }); + } + checkOutputContains(host, allEmittedFiles); + + const emittedFiles = getEmittedFiles(files, initialEmittedFileContents); + verifyEmittedFiles(host, emittedFiles); + host.clearOutput(); + + const affectedFiles = modifyFiles(files, emittedFiles); + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedLines(host, affectedFiles, allEmittedFiles); + + verifyEmittedFiles(host, emittedFiles); + } + + function verifyNewLine(newLine: string) { + const lines = ["var x = 1;", "var y = 2;"]; + const fileContent = lines.join(newLine); + const f = { + path: "/a/app.ts", + content: fileContent + }; + + verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content = fileContent + newLine + "var z = 3;"; + emittedFiles[0].content = files[0].content + newLine; + return [files[0]]; + } + } + + it("handles new lines: \\n", () => { + verifyNewLine("\n"); + }); + + it("handles new lines: \\r\\n", () => { + verifyNewLine("\r\n"); + }); + + it("should emit specified file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export function Foo() { return 10; }` + }; + + const file2 = { + path: "/a/b/f2.ts", + content: `import {Foo} from "./f1"; export let y = Foo();` + }; + + const file3 = { + path: "/a/b/f3.ts", + content: `import {y} from "./f2"; let x = y;` + }; + + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) + }; + + verifyEmittedFileContents("\r\n", [file1, file2, file3], [ + `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, + `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, + `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n` + ], modifyFiles, configFile); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content += `export function foo2() { return 2; }`; + emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`; + emittedFiles[2].shouldBeWritten = false; + return files.slice(0, 2); + } + }); + + it("Elides const enums correctly in incremental compilation", () => { + const currentDirectory = "/user/someone/projects/myproject"; + const file1: File = { + path: `${currentDirectory}/file1.ts`, + content: "export const enum E1 { V = 1 }" + }; + const file2: File = { + path: `${currentDirectory}/file2.ts`, + content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` + }; + const file3: File = { + path: `${currentDirectory}/file3.ts`, + content: `import { E2 } from "./file2"; const v: E2 = E2.V;` + }; + const strictAndEsModule = `"use strict";\nexports.__esModule = true;\n`; + verifyEmittedFileContents("\n", [file3, file2, file1], [ + `${strictAndEsModule}var v = 1 /* V */;\n`, + strictAndEsModule, + strictAndEsModule + ], modifyFiles); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content += `function foo2() { return 2; }`; + emittedFiles[0].content += `function foo2() { return 2; }\n`; + emittedFiles[1].shouldBeWritten = false; + emittedFiles[2].shouldBeWritten = false; + return [files[0]]; + } + }); + + it("file is deleted and created as part of change", () => { + const projectLocation = "/home/username/project"; + const file: File = { + path: `${projectLocation}/app/file.ts`, + content: "var a = 10;" + }; + const fileJs = `${projectLocation}/app/file.js`; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + include: [ + "app/**/*.ts" + ] + }) + }; + const files = [file, configFile, libFile]; + const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); + createWatchOfConfigFile("tsconfig.json", host); + verifyProgram(); + + file.content += "\nvar b = 10;"; + + host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true }); + host.runQueuedTimeoutCallbacks(); + verifyProgram(); + + function verifyProgram() { + assert.isTrue(host.fileExists(fileJs)); + assert.equal(host.readFile(fileJs), file.content + "\n"); + } + }); + }); + + describe("unittests:: tsc-watch:: emit with when module emit is specified as node", () => { + it("when instead of filechanged recursive directory watcher is invoked", () => { + const configFile: File = { + path: "/a/rootFolder/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + module: "none", + allowJs: true, + outDir: "Static/scripts/" + }, + include: [ + "Scripts/**/*" + ], + }) + }; + const outputFolder = "/a/rootFolder/project/Static/scripts/"; + const file1: File = { + path: "/a/rootFolder/project/Scripts/TypeScript.ts", + content: "var z = 10;" + }; + const file2: File = { + path: "/a/rootFolder/project/Scripts/Javascript.js", + content: "var zz = 10;" + }; + const files = [configFile, file1, file2, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host); + + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + file1.content = "var zz30 = 100;"; + host.reloadFS(files, { invokeDirectoryWatcherInsteadOfFileChanged: true }); + host.runQueuedTimeoutCallbacks(); + + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + const outputFile1 = changeExtension((outputFolder + getBaseFileName(file1.path)), ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + }); + }); +} diff --git a/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts b/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts new file mode 100644 index 00000000000..b76c9a985d4 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts @@ -0,0 +1,307 @@ +namespace ts.tscWatch { + describe("unittests:: tsc-watch:: Emit times and Error updates in builder after program changes", () => { + const currentDirectory = "/user/username/projects/myproject"; + const config: File = { + path: `${currentDirectory}/tsconfig.json`, + content: `{}` + }; + function getOutputFileStampAndError(host: WatchedSystem, watch: Watch, file: File) { + const builderProgram = watch.getBuilderProgram(); + const state = builderProgram.getState(); + return { + file, + fileStamp: host.getModifiedTime(file.path.replace(".ts", ".js")), + errors: builderProgram.getSemanticDiagnostics(watch().getSourceFileByPath(file.path as Path)), + errorsFromOldState: !!state.semanticDiagnosticsFromOldState && state.semanticDiagnosticsFromOldState.has(file.path) + }; + } + + function getOutputFileStampsAndErrors(host: WatchedSystem, watch: Watch, directoryFiles: ReadonlyArray) { + return directoryFiles.map(d => getOutputFileStampAndError(host, watch, d)); + } + + function findStampAndErrors(stampsAndErrors: ReadonlyArray>, file: File) { + return find(stampsAndErrors, info => info.file === file)!; + } + + function verifyOutputFileStampsAndErrors( + file: File, + emitExpected: boolean, + errorRefershExpected: boolean, + beforeChangeFileStampsAndErrors: ReadonlyArray>, + afterChangeFileStampsAndErrors: ReadonlyArray> + ) { + const beforeChange = findStampAndErrors(beforeChangeFileStampsAndErrors, file); + const afterChange = findStampAndErrors(afterChangeFileStampsAndErrors, file); + if (emitExpected) { + assert.notStrictEqual(afterChange.fileStamp, beforeChange.fileStamp, `Expected emit for file ${file.path}`); + } + else { + assert.strictEqual(afterChange.fileStamp, beforeChange.fileStamp, `Did not expect new emit for file ${file.path}`); + } + if (errorRefershExpected) { + if (afterChange.errors !== emptyArray || beforeChange.errors !== emptyArray) { + assert.notStrictEqual(afterChange.errors, beforeChange.errors, `Expected new errors for file ${file.path}`); + } + assert.isFalse(afterChange.errorsFromOldState, `Expected errors to be not copied from old state for file ${file.path}`); + } + else { + assert.strictEqual(afterChange.errors, beforeChange.errors, `Expected errors to not change for file ${file.path}`); + assert.isTrue(afterChange.errorsFromOldState, `Expected errors to be copied from old state for file ${file.path}`); + } + } + + interface VerifyEmitAndErrorUpdates { + change: (host: WatchedSystem) => void; + getInitialErrors: (watch: Watch) => ReadonlyArray | ReadonlyArray; + getIncrementalErrors: (watch: Watch) => ReadonlyArray | ReadonlyArray; + filesWithNewEmit: ReadonlyArray; + filesWithOnlyErrorRefresh: ReadonlyArray; + filesNotTouched: ReadonlyArray; + configFile?: File; + } + + function verifyEmitAndErrorUpdates({ filesWithNewEmit, filesWithOnlyErrorRefresh, filesNotTouched, configFile = config, change, getInitialErrors, getIncrementalErrors }: VerifyEmitAndErrorUpdates) { + const nonLibFiles = [...filesWithNewEmit, ...filesWithOnlyErrorRefresh, ...filesNotTouched]; + const files = [...nonLibFiles, configFile, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfConfigFile("tsconfig.json", host); + checkProgramActualFiles(watch(), [...nonLibFiles.map(f => f.path), libFile.path]); + checkOutputErrorsInitial(host, getInitialErrors(watch)); + const beforeChange = getOutputFileStampsAndErrors(host, watch, nonLibFiles); + change(host); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, getIncrementalErrors(watch)); + const afterChange = getOutputFileStampsAndErrors(host, watch, nonLibFiles); + filesWithNewEmit.forEach(file => verifyOutputFileStampsAndErrors(file, /*emitExpected*/ true, /*errorRefershExpected*/ true, beforeChange, afterChange)); + filesWithOnlyErrorRefresh.forEach(file => verifyOutputFileStampsAndErrors(file, /*emitExpected*/ false, /*errorRefershExpected*/ true, beforeChange, afterChange)); + filesNotTouched.forEach(file => verifyOutputFileStampsAndErrors(file, /*emitExpected*/ false, /*errorRefershExpected*/ false, beforeChange, afterChange)); + } + + describe("deep import changes", () => { + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `import {B} from './b'; +declare var console: any; +let b = new B(); +console.log(b.c.d);` + }; + + function verifyDeepImportChange(bFile: File, cFile: File) { + const filesWithNewEmit: File[] = []; + const filesWithOnlyErrorRefresh = [aFile]; + addImportedModule(bFile); + addImportedModule(cFile); + verifyEmitAndErrorUpdates({ + filesWithNewEmit, + filesWithOnlyErrorRefresh, + filesNotTouched: emptyArray, + change: host => host.writeFile(cFile.path, cFile.content.replace("d", "d2")), + getInitialErrors: () => emptyArray, + getIncrementalErrors: watch => [ + getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.lastIndexOf("d"), 1, Diagnostics.Property_0_does_not_exist_on_type_1, "d", "C") + ] + }); + + function addImportedModule(file: File) { + if (file.path.endsWith(".d.ts")) { + filesWithOnlyErrorRefresh.push(file); + } + else { + filesWithNewEmit.push(file); + } + } + } + + it("updates errors when deep import file changes", () => { + const bFile: File = { + path: `${currentDirectory}/b.ts`, + content: `import {C} from './c'; +export class B +{ + c = new C(); +}` + }; + const cFile: File = { + path: `${currentDirectory}/c.ts`, + content: `export class C +{ + d = 1; +}` + }; + verifyDeepImportChange(bFile, cFile); + }); + + it("updates errors when deep import through declaration file changes", () => { + const bFile: File = { + path: `${currentDirectory}/b.d.ts`, + content: `import {C} from './c'; +export class B +{ + c: C; +}` + }; + const cFile: File = { + path: `${currentDirectory}/c.d.ts`, + content: `export class C +{ + d: number; +}` + }; + verifyDeepImportChange(bFile, cFile); + }); + }); + + it("updates errors in file not exporting a deep multilevel import that changes", () => { + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `export interface Point { + name: string; + c: Coords; +} +export interface Coords { + x2: number; + y: number; +}` + }; + const bFile: File = { + path: `${currentDirectory}/b.ts`, + content: `import { Point } from "./a"; +export interface PointWrapper extends Point { +}` + }; + const cFile: File = { + path: `${currentDirectory}/c.ts`, + content: `import { PointWrapper } from "./b"; +export function getPoint(): PointWrapper { + return { + name: "test", + c: { + x: 1, + y: 2 + } + } +};` + }; + const dFile: File = { + path: `${currentDirectory}/d.ts`, + content: `import { getPoint } from "./c"; +getPoint().c.x;` + }; + const eFile: File = { + path: `${currentDirectory}/e.ts`, + content: `import "./d";` + }; + verifyEmitAndErrorUpdates({ + filesWithNewEmit: [aFile, bFile], + filesWithOnlyErrorRefresh: [cFile, dFile], + filesNotTouched: [eFile], + change: host => host.writeFile(aFile.path, aFile.content.replace("x2", "x")), + getInitialErrors: watch => [ + getDiagnosticOfFileFromProgram(watch(), cFile.path, cFile.content.indexOf("x: 1"), 4, chainDiagnosticMessages( + chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, "x", "Coords"), + Diagnostics.Type_0_is_not_assignable_to_type_1, + "{ x: number; y: number; }", + "Coords" + )), + getDiagnosticOfFileFromProgram(watch(), dFile.path, dFile.content.lastIndexOf("x"), 1, Diagnostics.Property_0_does_not_exist_on_type_1, "x", "Coords") + ], + getIncrementalErrors: () => emptyArray + }); + }); + + describe("updates errors when file transitively exported file changes", () => { + const config: File = { + path: `${currentDirectory}/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts"], + compilerOptions: { baseUrl: "." } + }) + }; + const app: File = { + path: `${currentDirectory}/app.ts`, + content: `import { Data } from "lib2/public"; +export class App { + public constructor() { + new Data().test(); + } +}` + }; + const lib2Public: File = { + path: `${currentDirectory}/lib2/public.ts`, + content: `export * from "./data";` + }; + const lib2Data: File = { + path: `${currentDirectory}/lib2/data.ts`, + content: `import { ITest } from "lib1/public"; +export class Data { + public test() { + const result: ITest = { + title: "title" + } + return result; + } +}` + }; + const lib1Public: File = { + path: `${currentDirectory}/lib1/public.ts`, + content: `export * from "./tools/public";` + }; + const lib1ToolsPublic: File = { + path: `${currentDirectory}/lib1/tools/public.ts`, + content: `export * from "./tools.interface";` + }; + const lib1ToolsInterface: File = { + path: `${currentDirectory}/lib1/tools/tools.interface.ts`, + content: `export interface ITest { + title: string; +}` + }; + + function verifyTransitiveExports(lib2Data: File, lib2Data2?: File) { + const filesWithNewEmit = [lib1ToolsInterface, lib1ToolsPublic]; + const filesWithOnlyErrorRefresh = [app, lib2Public, lib1Public, lib2Data]; + if (lib2Data2) { + filesWithOnlyErrorRefresh.push(lib2Data2); + } + verifyEmitAndErrorUpdates({ + filesWithNewEmit, + filesWithOnlyErrorRefresh, + filesNotTouched: emptyArray, + configFile: config, + change: host => host.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), + getInitialErrors: () => emptyArray, + getIncrementalErrors: () => [ + "lib2/data.ts(5,13): error TS2322: Type '{ title: string; }' is not assignable to type 'ITest'.\n Object literal may only specify known properties, but 'title' does not exist in type 'ITest'. Did you mean to write 'title2'?\n" + ] + }); + } + it("when there are no circular import and exports", () => { + verifyTransitiveExports(lib2Data); + }); + + it("when there are circular import and exports", () => { + const lib2Data: File = { + path: `${currentDirectory}/lib2/data.ts`, + content: `import { ITest } from "lib1/public"; import { Data2 } from "./data2"; +export class Data { + public dat?: Data2; public test() { + const result: ITest = { + title: "title" + } + return result; + } +}` + }; + const lib2Data2: File = { + path: `${currentDirectory}/lib2/data2.ts`, + content: `import { Data } from "./data"; +export class Data2 { + public dat?: Data; +}` + }; + verifyTransitiveExports(lib2Data, lib2Data2); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts new file mode 100644 index 00000000000..b593cabbec6 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -0,0 +1,203 @@ +namespace ts.tscWatch { + export import WatchedSystem = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export import libFile = TestFSWithWatch.libFile; + export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; + export import checkArray = TestFSWithWatch.checkArray; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + export import checkOutputContains = TestFSWithWatch.checkOutputContains; + export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; + + export const commonFile1: File = { + path: "/a/b/commonFile1.ts", + content: "let x = 1" + }; + export const commonFile2: File = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" + }; + + export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + } + + export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray) { + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + } + + export interface Watch { + (): Program; + getBuilderProgram(): EmitAndSemanticDiagnosticsBuilderProgram; + } + + export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + const result = (() => watch.getCurrentProgram().getProgram()) as Watch; + result.getBuilderProgram = () => watch.getCurrentProgram(); + return result; + } + + export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); + } + + const elapsedRegex = /^Elapsed:: [0-9]+ms/; + function checkOutputErrors( + host: WatchedSystem, + logsBeforeWatchDiagnostic: string[] | undefined, + preErrorsWatchDiagnostic: Diagnostic, + logsBeforeErrors: string[] | undefined, + errors: ReadonlyArray | ReadonlyArray, + disableConsoleClears?: boolean | undefined, + ...postErrorsWatchDiagnostics: Diagnostic[] + ) { + let screenClears = 0; + const outputs = host.getOutput(); + const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length + + (logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0); + assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs)); + let index = 0; + forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log)); + assertWatchDiagnostic(preErrorsWatchDiagnostic); + forEach(logsBeforeErrors, log => assertLog("logBeforeError", log)); + // Verify errors + forEach(errors, assertDiagnostic); + forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); + assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); + host.clearOutput(); + + function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { + return !!(diagnostic as Diagnostic).messageText; + } + + function assertDiagnostic(diagnostic: Diagnostic | string) { + const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; + assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); + index++; + } + + function assertLog(caption: string, expected: string) { + const actual = outputs[index]; + assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected)); + index++; + } + + function assertWatchDiagnostic(diagnostic: Diagnostic) { + const expected = getWatchDiagnosticWithoutDate(diagnostic); + if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { + assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); + screenClears++; + } + assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); + index++; + } + + function getOutputAtFailedMessage(caption: string, expectedOutput: string) { + return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; + } + + function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { + const newLines = contains(screenStartingMessageCodes, diagnostic.code) + ? `${host.newLine}${host.newLine}` + : host.newLine; + return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; + } + } + + function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray | ReadonlyArray) { + return errors.length === 1 + ? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) + : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); + } + + export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + /*logsBeforeWatchDiagnostic*/ undefined, + createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode), + logsBeforeErrors, + errors, + disableConsoleClears, + createErrorsFoundCompilerDiagnostic(errors)); + } + + export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + logsBeforeWatchDiagnostic, + createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), + logsBeforeErrors, + errors, + disableConsoleClears, + createErrorsFoundCompilerDiagnostic(errors)); + } + + export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors( + host, + logsBeforeWatchDiagnostic, + createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), + logsBeforeErrors, + errors, + disableConsoleClears); + assert.equal(host.exitCode, expectedExitCode); + } + + function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain { + return !!(message as DiagnosticMessageChain).messageText; + } + + export function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ..._args: (string | number)[]): Diagnostic { + let text: DiagnosticMessageChain | string; + if (isDiagnosticMessageChain(message)) { + text = message; + } + else { + text = getLocaleSpecificMessage(message); + if (arguments.length > 4) { + text = formatStringFromArgs(text, arguments, 4); + } + } + return { + file, + start, + length, + + messageText: text, + category: message.category, + code: message.code, + }; + } + + export function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { + return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); + } + + export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { + return getDiagnosticOfFileFrom(file, start, length, message, ...args); + } + + export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { + return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!, + start, length, message, ...args); + } + + export function getUnknownCompilerOption(program: Program, configFile: File, option: string) { + const quotedOption = `"${option}"`; + return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); + } + + export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { + const quotedModuleName = `"${moduleName}"`; + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); + } +} diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts new file mode 100644 index 00000000000..45ff6fba7c5 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -0,0 +1,1287 @@ +namespace ts.tscWatch { + describe("unittests:: tsc-watch:: program updates", () => { + it("create watch without config file", () => { + const appFile: File = { + path: "/a/b/c/app.ts", + content: ` + import {f} from "./module" + console.log(f) + ` + }; + + const moduleFile: File = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + const host = createWatchedSystem([appFile, moduleFile, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([appFile.path], host); + + checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); + + // TODO: Should we watch creation of config files in the root file's file hierarchy? + + // const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + // const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + // checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + }); + + it("can handle tsconfig file name with difference casing", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + include: ["app.ts"] + }) + }; + + const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); + const watch = createWatchOfConfigFile(upperCaseConfigFilePath, host); + checkProgramActualFiles(watch(), [combinePaths(getDirectoryPath(upperCaseConfigFilePath), getBaseFileName(f1.path))]); + }); + + it("create configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "exclude": [ + "e" + ] + }` + }; + const file1: File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); + const watch = createWatchProgram(createWatchCompilerHostOfConfigFile(configFile.path, {}, host, /*createProgram*/ undefined, notImplemented)); + + checkProgramActualFiles(watch.getCurrentProgram().getProgram(), [file1.path, libFile.path, file2.path]); + checkProgramRootFiles(watch.getCurrentProgram().getProgram(), [file1.path, file2.path]); + checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); + const configDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); + }); + + // TODO: if watching for config file creation + // it("add and then remove a config file in a folder with loose files", () => { + // }); + + it("add new files to a configured program without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([commonFile1, libFile, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + const configDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); + + checkProgramRootFiles(watch(), [commonFile1.path]); + + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = createWatchedSystem([commonFile1, commonFile2, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + + const commonFile3 = "/a/b/commonFile3.ts"; + checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); + checkProgramActualFiles(watch(), [commonFile1.path]); + }); + + it("handle recreated files correctly", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([commonFile1, commonFile2, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path]); + + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("handles the missing files - that were added to program because they were added with /// { + const commonFile2Name = "commonFile2.ts"; + const file1: File = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createWatchedSystem([file1, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path]); + checkOutputErrorsInitial(host, [ + getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf(commonFile2Name), commonFile2Name.length, Diagnostics.File_0_not_found, commonFile2.path), + getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf("y"), 1, Diagnostics.Cannot_find_name_0, "y") + ]); + + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("should reflect change in config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host); + + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + configFile.content = `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}"] + }`; + + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + checkProgramRootFiles(watch(), [commonFile1.path]); + }); + + it("works correctly when config file is changed but its content havent", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host); + + checkProgramActualFiles(watch(), [libFile.path, commonFile1.path, commonFile2.path]); + checkOutputErrorsInitial(host, emptyArray); + + host.modifyFile(configFile.path, configFile.content); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + + checkProgramActualFiles(watch(), [libFile.path, commonFile1.path, commonFile2.path]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("Updates diagnostics when '--noUnusedLabels' changes", () => { + const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; + const files = [libFile, aTs]; + const paths = files.map(f => f.path); + const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; + const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; + + const host = createWatchedSystem([...files, tsconfig]); + const watch = createWatchOfConfigFile(tsconfig.path, host); + + checkProgramActualFiles(watch(), paths); + checkOutputErrorsInitial(host, emptyArray); + + host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + + checkProgramActualFiles(watch(), paths); + checkOutputErrorsIncremental(host, [ + getDiagnosticOfFileFromProgram(watch(), aTs.path, 0, "label".length, Diagnostics.Unused_label), + ]); + + host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ true)); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + checkProgramActualFiles(watch(), paths); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("files explicitly excluded in config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("should properly handle module resolution changes in config file", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); + + configFile.content = `{ + "compilerOptions": { + "moduleResolution": "classic" + }, + "files": ["${file1.path}"] + }`; + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, classicModuleFile.path]); + }); + + it("should tolerate config file errors and still try to build a project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6", + "allowAnything": true + }, + "someOtherProperty": {} + }` + }; + const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("changes in files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + + host.reloadFS([file1, modifiedFile2, file3]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, modifiedFile2.path, file3.path]); + }); + + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path]); + }); + + it("deleted files affect project structure - 2", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchOfFilesAndCompilerOptions([file1.path, file3.path], host); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path, file3.path]); + }); + + it("config file includes the file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const file2 = { + path: "/a/c/f2.ts", + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: "export let y = 1" + }; + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, file3, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + + checkProgramRootFiles(watch(), [file2.path, file3.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + }); + + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchOfFilesAndCompilerOptions([file2.path, file3.path], host); + checkProgramActualFiles(watch(), [file2.path, file3.path]); + + const watch2 = createWatchOfFilesAndCompilerOptions([file1.path], host); + checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); + + // Previous program shouldnt be updated + checkProgramActualFiles(watch(), [file2.path, file3.path]); + host.checkTimeoutQueueLength(0); + }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + + const host = createWatchedSystem([file1, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), [file1.path]); + + host.reloadFS([file1, file2, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path, file2.path]); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + + checkProgramActualFiles(watch(), [file1.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + }); + + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, configFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + }); + + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createWatchedSystem([file1, file2, libFile, config]); + const watch = createWatchOfConfigFile(config.path, host); + + checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); + checkOutputErrorsInitial(host, emptyArray); + + host.reloadFS([file1, file2, libFile]); + host.checkTimeoutQueueLengthAndRun(1); + + checkOutputErrorsIncrementalWithExit(host, [ + getDiagnosticWithoutFile(Diagnostics.File_0_not_found, config.path) + ], ExitStatus.DiagnosticsPresent_OutputsSkipped); + }); + + it("Proper errors: document is not contained in project", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: "/a/b/tsconfig.json", + content: "{" + }; + const host = createWatchedSystem([file1, corruptedConfig]); + const watch = createWatchOfConfigFile(corruptedConfig.path, host); + + checkProgramActualFiles(watch(), [file1.path]); + }); + + it("correctly handles changes in lib section of config file", () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5" + ] + } + }) + }; + const config2 = { + path: config1.path, + content: JSON.stringify( + { + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5", + "es2015.promise" + ] + } + }) + }; + const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const watch = createWatchOfConfigFile(config1.path, host); + + checkProgramActualFiles(watch(), [libES5.path, app.path]); + + host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), [libES5.path, libES2015Promise.path, app.path]); + }); + + it("should handle non-existing directories in config file", () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + const host = createWatchedSystem([f, config]); + const watch = createWatchOfConfigFile(config.path, host); + checkProgramActualFiles(watch(), [f.path]); + }); + + it("rename a module file and rename back should restore the states for inferred projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: 'import * as T from "./moduleFile"; T.bar();' + }; + const host = createWatchedSystem([moduleFile, file1, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + checkOutputErrorsInitial(host, emptyArray); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") + ]); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("rename a module file and rename back should restore the states for configured projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: 'import * as T from "./moduleFile"; T.bar();' + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkOutputErrorsInitial(host, emptyArray); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") + ]); + + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("types should load from config file path if config exists", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) + }; + const node = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: "declare var process: any" + }; + const cwd = { + path: "/a/c" + }; + const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); + const watch = createWatchOfConfigFile(config.path, host); + + checkProgramActualFiles(watch(), [f1.path, node.path]); + }); + + it("add the missing module file for inferred project: should remove the `module not found` error", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: 'import * as T from "./moduleFile"; T.bar();' + }; + const host = createWatchedSystem([file1, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") + ]); + + host.reloadFS([file1, moduleFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("Configure file diagnostics events are generated when the config file has errors", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkOutputErrorsInitial(host, [ + getUnknownCompilerOption(watch(), configFile, "foo"), + getUnknownCompilerOption(watch(), configFile, "allowJS") + ]); + }); + + it("If config file doesnt have errors, they are not reported", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchOfConfigFile(configFile.path, host); + checkOutputErrorsInitial(host, emptyArray); + }); + + it("Reports errors when the config file changes", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + checkOutputErrorsInitial(host, emptyArray); + + configFile.content = `{ + "compilerOptions": { + "haha": 123 + } + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, [ + getUnknownCompilerOption(watch(), configFile, "haha") + ]); + + configFile.content = `{ + "compilerOptions": {} + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "include": ["app/*", "test/**/*", "something"] + }` + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + + const host = createWatchedSystem([file1, configFile, libFile]); + const watch = createWatchOfConfigFile(configFile.path, host); + + checkProgramActualFiles(watch(), emptyArray); + checkOutputErrorsInitial(host, [ + "error TS18003: No inputs were found in config file '/a/b/tsconfig.json'. Specified 'include' paths were '[\"app/*\",\"test/**/*\",\"something\"]' and 'exclude' paths were '[]'.\n" + ]); + }); + + it("non-existing directories listed in config file input array should be able to handle @types if input file list is empty", () => { + const f = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); + const watch = createWatchOfConfigFile(config.path, host); + + checkProgramActualFiles(watch(), emptyArray); + checkOutputErrorsInitial(host, [ + "tsconfig.json(1,24): error TS18002: The 'files' list in config file '/a/tsconfig.json' is empty.\n" + ]); + }); + + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createWatchedSystem([f, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([f.path], host, { allowNonTsExtensions: true }); + checkProgramActualFiles(watch(), [f.path, libFile.path]); + }); + + it("Options Diagnostic locations reported correctly with changes in configFile contents when options change", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFileContentBeforeComment = `{`; + const configFileContentComment = ` + // comment + // More comment`; + const configFileContentAfterComment = ` + "compilerOptions": { + "allowJs": true, + "declaration": true + } + }`; + const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; + const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; + const configFile = { + path: "/a/b/tsconfig.json", + content: configFileContentWithComment + }; + + const files = [file, libFile, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host); + const errors = () => [ + getDiagnosticOfFile(watch().getCompilerOptions().configFile!, configFile.content.indexOf('"allowJs"'), '"allowJs"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration"), + getDiagnosticOfFile(watch().getCompilerOptions().configFile!, configFile.content.indexOf('"declaration"'), '"declaration"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration") + ]; + const intialErrors = errors(); + checkOutputErrorsInitial(host, intialErrors); + + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + const nowErrors = errors(); + checkOutputErrorsIncremental(host, nowErrors); + assert.equal(nowErrors[0].start, intialErrors[0].start! - configFileContentComment.length); + assert.equal(nowErrors[1].start, intialErrors[1].start! - configFileContentComment.length); + }); + + describe("should not trigger should not trigger recompilation because of program emit", () => { + function verifyWithOptions(options: CompilerOptions, outputFiles: ReadonlyArray) { + const proj = "/user/username/projects/myproject"; + const file1: File = { + path: `${proj}/file1.ts`, + content: "export const c = 30;" + }; + const file2: File = { + path: `${proj}/src/file2.ts`, + content: `import {c} from "file1"; export const d = 30;` + }; + const tsconfig: File = { + path: `${proj}/tsconfig.json`, + content: generateTSConfig(options, emptyArray, "\n") + }; + const host = createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: proj }); + const watch = createWatchOfConfigFile(tsconfig.path, host, /*maxNumberOfFilesToIterateForInvalidation*/1); + checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); + + outputFiles.forEach(f => host.fileExists(f)); + + // This should be 0 + host.checkTimeoutQueueLengthAndRun(0); + } + + it("without outDir or outFile is specified", () => { + verifyWithOptions({ module: ModuleKind.AMD }, ["file1.js", "src/file2.js"]); + }); + + it("with outFile", () => { + verifyWithOptions({ module: ModuleKind.AMD, outFile: "build/outFile.js" }, ["build/outFile.js"]); + }); + + it("when outDir is specified", () => { + verifyWithOptions({ module: ModuleKind.AMD, outDir: "build" }, ["build/file1.js", "build/src/file2.js"]); + }); + + it("when outDir and declarationDir is specified", () => { + verifyWithOptions({ module: ModuleKind.AMD, outDir: "build", declaration: true, declarationDir: "decls" }, + ["build/file1.js", "build/src/file2.js", "decls/file1.d.ts", "decls/src/file2.d.ts"]); + }); + + it("declarationDir is specified", () => { + verifyWithOptions({ module: ModuleKind.AMD, declaration: true, declarationDir: "decls" }, + ["file1.js", "src/file2.js", "decls/file1.d.ts", "decls/src/file2.d.ts"]); + }); + }); + + it("shouldnt report error about unused function incorrectly when file changes from global to module", () => { + const getFileContent = (asModule: boolean) => ` + function one() {} + ${asModule ? "export " : ""}function two() { + return function three() { + one(); + } + }`; + const file: File = { + path: "/a/b/file.ts", + content: getFileContent(/*asModule*/ false) + }; + const files = [file, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([file.path], host, { + noUnusedLocals: true + }); + checkProgramActualFiles(watch(), files.map(file => file.path)); + checkOutputErrorsInitial(host, []); + + file.content = getFileContent(/*asModule*/ true); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), files.map(file => file.path)); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("watched files when file is deleted and new file is added as part of change", () => { + const projectLocation = "/home/username/project"; + const file: File = { + path: `${projectLocation}/src/file1.ts`, + content: "var a = 10;" + }; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: "{}" + }; + const files = [file, libFile, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfConfigFile(configFile.path, host); + verifyProgram(); + + file.path = file.path.replace("file1", "file2"); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProgram(); + + function verifyProgram() { + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [projectLocation, `${projectLocation}/node_modules/@types`], /*recursive*/ true); + checkWatchedFiles(host, files.map(f => f.path)); + } + }); + + it("updates errors correctly when declaration emit is disabled in compiler options", () => { + const currentDirectory = "/user/username/projects/myproject"; + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `import test from './b'; +test(4, 5);` + }; + const bFileContent = `function test(x: number, y: number) { + return x + y / 5; +} +export default test;`; + const bFile: File = { + path: `${currentDirectory}/b.ts`, + content: bFileContent + }; + const tsconfigFile: File = { + path: `${currentDirectory}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "commonjs", + noEmit: true, + strict: true, + } + }) + }; + const files = [aFile, bFile, libFile, tsconfigFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfConfigFile("tsconfig.json", host); + checkOutputErrorsInitial(host, emptyArray); + + changeParameterType("x", "string", [ + getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.indexOf("4"), 1, Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1, "4", "string") + ]); + changeParameterType("y", "string", [ + getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.indexOf("5"), 1, Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1, "5", "string"), + getDiagnosticOfFileFromProgram(watch(), bFile.path, bFile.content.indexOf("y /"), 1, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type) + ]); + + function changeParameterType(parameterName: string, toType: string, expectedErrors: ReadonlyArray) { + const newContent = bFileContent.replace(new RegExp(`${parameterName}\: [a-z]*`), `${parameterName}: ${toType}`); + + verifyErrorsWithBFileContents(newContent, expectedErrors); + verifyErrorsWithBFileContents(bFileContent, emptyArray); + } + + function verifyErrorsWithBFileContents(content: string, expectedErrors: ReadonlyArray) { + host.writeFile(bFile.path, content); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, expectedErrors); + } + }); + + it("updates errors when strictNullChecks changes", () => { + const currentDirectory = "/user/username/projects/myproject"; + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `declare function foo(): null | { hello: any }; +foo().hello` + }; + const config: File = { + path: `${currentDirectory}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + const files = [aFile, config, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfConfigFile("tsconfig.json", host); + checkProgramActualFiles(watch(), [aFile.path, libFile.path]); + checkOutputErrorsInitial(host, emptyArray); + const modifiedTimeOfAJs = host.getModifiedTime(`${currentDirectory}/a.js`); + host.writeFile(config.path, JSON.stringify({ compilerOptions: { strictNullChecks: true } })); + host.runQueuedTimeoutCallbacks(); + const expectedStrictNullErrors = [ + getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.lastIndexOf("foo()"), 5, Diagnostics.Object_is_possibly_null) + ]; + checkOutputErrorsIncremental(host, expectedStrictNullErrors); + // File a need not be rewritten + assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); + host.writeFile(config.path, JSON.stringify({ compilerOptions: { strict: true, alwaysStrict: false } })); // Avoid changing 'alwaysStrict' or must re-bind + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, expectedStrictNullErrors); + // File a need not be rewritten + assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); + host.writeFile(config.path, JSON.stringify({ compilerOptions: {} })); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + // File a need not be rewritten + assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); + }); + + it("updates errors when ambient modules of program changes", () => { + const currentDirectory = "/user/username/projects/myproject"; + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `declare module 'a' { + type foo = number; +}` + }; + const config: File = { + path: `${currentDirectory}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, config, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfConfigFile("tsconfig.json", host); + checkProgramActualFiles(watch(), [aFile.path, libFile.path]); + checkOutputErrorsInitial(host, emptyArray); + + // Create bts with same file contents + const bTsPath = `${currentDirectory}/b.ts`; + host.writeFile(bTsPath, aFile.content); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [aFile.path, "b.ts", libFile.path]); + checkOutputErrorsIncremental(host, [ + "a.ts(2,8): error TS2300: Duplicate identifier 'foo'.\n", + "b.ts(2,8): error TS2300: Duplicate identifier 'foo'.\n" + ]); + + // Delete bTs + host.deleteFile(bTsPath); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [aFile.path, libFile.path]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + describe("updates errors in lib file", () => { + const currentDirectory = "/user/username/projects/myproject"; + const field = "fullscreen"; + const fieldWithoutReadonly = `interface Document { + ${field}: boolean; +}`; + + const libFileWithDocument: File = { + path: libFile.path, + content: `${libFile.content} +interface Document { + readonly ${field}: boolean; +}` + }; + + function getDiagnostic(program: Program, file: File) { + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(field), field.length, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, field); + } + + function verifyLibFileErrorsWith(aFile: File) { + const files = [aFile, libFileWithDocument]; + + function verifyLibErrors(options: CompilerOptions) { + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfFilesAndCompilerOptions([aFile.path], host, options); + checkProgramActualFiles(watch(), [aFile.path, libFile.path]); + checkOutputErrorsInitial(host, getErrors()); + + host.writeFile(aFile.path, aFile.content.replace(fieldWithoutReadonly, "var x: string;")); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [aFile.path, libFile.path]); + checkOutputErrorsIncremental(host, emptyArray); + + host.writeFile(aFile.path, aFile.content); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [aFile.path, libFile.path]); + checkOutputErrorsIncremental(host, getErrors()); + + function getErrors() { + return [ + ...(options.skipLibCheck || options.skipDefaultLibCheck ? [] : [getDiagnostic(watch(), libFileWithDocument)]), + getDiagnostic(watch(), aFile) + ]; + } + } + + it("with default options", () => { + verifyLibErrors({}); + }); + it("with skipLibCheck", () => { + verifyLibErrors({ skipLibCheck: true }); + }); + it("with skipDefaultLibCheck", () => { + verifyLibErrors({ skipDefaultLibCheck: true }); + }); + } + + describe("when non module file changes", () => { + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `${fieldWithoutReadonly} +var y: number;` + }; + verifyLibFileErrorsWith(aFile); + }); + + describe("when module file with global definitions changes", () => { + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `export {} +declare global { +${fieldWithoutReadonly} +var y: number; +}` + }; + verifyLibFileErrorsWith(aFile); + }); + }); + + it("when skipLibCheck and skipDefaultLibCheck changes", () => { + const currentDirectory = "/user/username/projects/myproject"; + const field = "fullscreen"; + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `interface Document { + ${field}: boolean; +}` + }; + const bFile: File = { + path: `${currentDirectory}/b.d.ts`, + content: `interface Document { + ${field}: boolean; +}` + }; + const libFileWithDocument: File = { + path: libFile.path, + content: `${libFile.content} +interface Document { + readonly ${field}: boolean; +}` + }; + const configFile: File = { + path: `${currentDirectory}/tsconfig.json`, + content: "{}" + }; + + const files = [aFile, bFile, configFile, libFileWithDocument]; + + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfConfigFile("tsconfig.json", host); + verifyProgramFiles(); + checkOutputErrorsInitial(host, [ + getDiagnostic(libFileWithDocument), + getDiagnostic(aFile), + getDiagnostic(bFile) + ]); + + verifyConfigChange({ skipLibCheck: true }, [aFile]); + verifyConfigChange({ skipDefaultLibCheck: true }, [aFile, bFile]); + verifyConfigChange({}, [libFileWithDocument, aFile, bFile]); + verifyConfigChange({ skipDefaultLibCheck: true }, [aFile, bFile]); + verifyConfigChange({ skipLibCheck: true }, [aFile]); + verifyConfigChange({}, [libFileWithDocument, aFile, bFile]); + + function verifyConfigChange(compilerOptions: CompilerOptions, errorInFiles: ReadonlyArray) { + host.writeFile(configFile.path, JSON.stringify({ compilerOptions })); + host.runQueuedTimeoutCallbacks(); + verifyProgramFiles(); + checkOutputErrorsIncremental(host, errorInFiles.map(getDiagnostic)); + } + + function getDiagnostic(file: File) { + return getDiagnosticOfFileFromProgram(watch(), file.path, file.content.indexOf(field), field.length, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, field); + } + + function verifyProgramFiles() { + checkProgramActualFiles(watch(), [aFile.path, bFile.path, libFile.path]); + } + }); + }); + + +} diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts new file mode 100644 index 00000000000..b29fde85697 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -0,0 +1,448 @@ +namespace ts.tscWatch { + describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { + it("works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; + + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); + const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); + + // ensure that imported file was found + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); + + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" + var x: string = 1;`; + root.content = newContent; + host.reloadFS(files); + + // patch fileExists to make sure that disk is not touched + host.fileExists = notImplemented; + + // trigger synchronization to make sure that import will be fetched from the cache + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + f1IsNotModule, + getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), + cannotFindFoo + ]); + } + { + let fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); + return originalFileExists.call(host, fileName); + }; + + root.content = `import {x} from "f2"`; + host.reloadFS(files); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + ]); + + assert.isTrue(fileExistsIsCalled); + } + { + let fileExistsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); + return originalFileExists.call(host, fileName); + }; + + const newContent = `import {x} from "f1"`; + root.content = newContent; + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); + assert.isTrue(fileExistsCalled); + } + }); + + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const host = createWatchedSystem(files); + const originalFileExists = host.fileExists; + + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + + const files = [root, libFile]; + const filesWithImported = files.concat(imported); + const host = createWatchedSystem(filesWithImported); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, emptyArray); + + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + + it("works when module resolution changes to ambient module", () => { + const root = { + path: "/a/b/foo.ts", + content: `import * as fs from "fs";` + }; + + const packageJson = { + path: "/a/b/node_modules/@types/node/package.json", + content: ` +{ + "main": "" +} +` + }; + + const nodeType = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +}` + }; + + const files = [root, libFile]; + const filesWithNodeType = files.concat(packageJson, nodeType); + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + host.reloadFS(filesWithNodeType); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when included file with ambient module changes", () => { + const root = { + path: "/a/b/foo.ts", + content: ` +import * as fs from "fs"; +import * as u from "url"; +` + }; + + const file = { + path: "/a/b/bar.d.ts", + content: ` +declare module "url" { + export interface Url { + href?: string; + } +} +` + }; + + const fileContentWithFS = ` +declare module "fs" { + export interface Stats { + isFile(): boolean; + } +} +`; + + const files = [root, file, libFile]; + const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); + + const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); + + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") + ]); + + file.content += fileContentWithFS; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + }); + + it("works when reusing program with files from external library", () => { + interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } + const configDir = "/a/b/projects/myProject/src/"; + const file1: File = { + path: configDir + "file1.ts", + content: 'import module1 = require("module1");\nmodule1("hello");' + }; + const file2: File = { + path: configDir + "file2.ts", + content: 'import module11 = require("module1");\nmodule11("hello");' + }; + const module1: File = { + path: "/a/b/projects/myProject/node_modules/module1/index.js", + content: "module.exports = options => { return options.toString(); }" + }; + const configFile: File = { + path: configDir + "tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + moduleResolution: "node", + maxNodeModuleJsDepth: 1 + } + }) + }; + const outDirFolder = "/a/b/projects/myProject/dist/"; + const programFiles = [file1, file2, module1, libFile]; + const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); + const watch = createWatchOfConfigFile(configFile.path, host); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsInitial(host, emptyArray); + const expectedFiles: ExpectedFile[] = [ + createExpectedEmittedFile(file1), + createExpectedEmittedFile(file2), + createExpectedToNotEmitFile("index.js"), + createExpectedToNotEmitFile("src/index.js"), + createExpectedToNotEmitFile("src/file1.js"), + createExpectedToNotEmitFile("src/file2.js"), + createExpectedToNotEmitFile("lib.js"), + createExpectedToNotEmitFile("lib.d.ts") + ]; + verifyExpectedFiles(expectedFiles); + + file1.content += "\n;"; + expectedFiles[0].content += ";\n"; // Only emit file1 with this change + expectedFiles[1].isExpectedToEmit = false; + host.reloadFS(programFiles.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrorsIncremental(host, emptyArray); + verifyExpectedFiles(expectedFiles); + + + function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { + forEach(expectedFiles, f => { + assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); + if (f.isExpectedToEmit) { + assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); + } + }); + } + + function createExpectedToNotEmitFile(fileName: string): ExpectedFile { + return { + path: outDirFolder + fileName, + isExpectedToEmit: false + }; + } + + function createExpectedEmittedFile(file: File): ExpectedFile { + return { + path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, + isExpectedToEmit: true, + content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" + }; + } + }); + + it("works when renaming node_modules folder that already contains @types folder", () => { + const currentDirectory = "/user/username/projects/myproject"; + const file: File = { + path: `${currentDirectory}/a.ts`, + content: `import * as q from "qqq";` + }; + const module: File = { + path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, + content: "export {}" + }; + const files = [file, module, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + const watch = createWatchOfFilesAndCompilerOptions([file.path], host); + + checkProgramActualFiles(watch(), [file.path, libFile.path]); + checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); + + host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); + host.runQueuedTimeoutCallbacks(); + checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); + checkOutputErrorsIncremental(host, emptyArray); + }); + + describe("ignores files/folder changes in node_modules that start with '.'", () => { + const projectPath = "/user/username/projects/project"; + const npmCacheFile: File = { + path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + const file1: File = { + path: `${projectPath}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectPath}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + const files = [libFile, file1, file2]; + const expectedFiles = files.map(f => f.path); + it("when watching node_modules in inferred project for failed lookup", () => { + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + it("when watching node_modules as part of wild card directories in config project", () => { + const config: File = { + path: `${projectPath}/tsconfig.json`, + content: "{}" + }; + const host = createWatchedSystem(files.concat(config)); + const watch = createWatchOfConfigFile(config.path, host); + checkProgramActualFiles(watch(), expectedFiles); + host.checkTimeoutQueueLength(0); + + host.ensureFileOrFolder(npmCacheFile); + host.checkTimeoutQueueLength(0); + }); + }); + }); + + describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch with modules linked to sibling folder", () => { + const projectRoot = "/user/username/projects/project"; + const mainPackageRoot = `${projectRoot}/main`; + const linkedPackageRoot = `${projectRoot}/linked-package`; + const mainFile: File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: File = { + path: `${linkedPackageRoot}/package.json`, + content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) + }; + const linkedPackageIndex: File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + + it("verify watched directories", () => { + const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); + }); + }); +} diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts new file mode 100644 index 00000000000..35527b73701 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -0,0 +1,40 @@ +namespace ts.tscWatch { + describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { + const projectRoot = "/user/username/projects/project"; + const configFileJson: any = { + compilerOptions: { module: "commonjs", resolveJsonModule: true }, + files: ["index.ts"] + }; + const mainFile: File = { + path: `${projectRoot}/index.ts`, + content: "import settings from './settings.json';" + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify(configFileJson) + }; + const settingsJson: File = { + path: `${projectRoot}/settings.json`, + content: JSON.stringify({ content: "Print this" }) + }; + + it("verify that module resolution with json extension works when returned without extension", () => { + const files = [libFile, mainFile, config, settingsJson]; + const host = createWatchedSystem(files, { currentDirectory: projectRoot }); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); + const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); + compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { + const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); + const resolvedModule = result.resolvedModule!; + return { + resolvedFileName: resolvedModule.resolvedFileName, + isExternalLibraryImport: resolvedModule.isExternalLibraryImport, + originalFileName: resolvedModule.originalPath, + }; + }); + const watch = createWatchProgram(compilerHost); + const program = watch.getCurrentProgram().getProgram(); + checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); + }); + }); +} diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts new file mode 100644 index 00000000000..d30e5854652 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -0,0 +1,178 @@ +namespace ts.tscWatch { + import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; + describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { + it("watchFile using dynamic priority polling", () => { + const projectFolder = "/a/username/project"; + const file1: File = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const files = [file1, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + + const initialProgram = watch(); + verifyProgram(); + + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; + for (let index = 0; index < mediumPollingIntervalThreshold; index++) { + // Transition libFile and file1 to low priority queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + } + + // Make a change to file + file1.content = "var zz30 = 100;"; + host.reloadFS(files); + + // This should detect change in the file + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + + // Callbacks: medium priority + high priority queue and scheduled program update + host.checkTimeoutQueueLengthAndRun(3); + // During this timeout the file would be detected as unchanged + let fileUnchangeDetected = 1; + const newProgram = watch(); + assert.notStrictEqual(newProgram, initialProgram); + + verifyProgram(); + const outputFile1 = changeExtension(file1.path, ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + + const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; + for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { + // For high + Medium/low polling interval + host.checkTimeoutQueueLengthAndRun(2); + assert.deepEqual(watch(), newProgram); + } + + // Everything goes in high polling interval queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), newProgram); + + function verifyProgram() { + checkProgramActualFiles(watch(), files.map(f => f.path)); + checkWatchedFiles(host, []); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [], /*recursive*/ true); + } + }); + + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfConfigFile(configFile.path, host); + const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; + // Watching files config file, file, lib file + const expectedWatchedFiles = files.map(f => f.path); + const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; + if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { + expectedWatchedFiles.push(...projectFolders); + } + + verifyProgram(checkOutputErrorsInitial); + + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { + // With dynamic polling the fs change would be detected only by running timeouts + host.runQueuedTimeoutCallbacks(); + } + // Delayed update program + host.runQueuedTimeoutCallbacks(); + verifyProgram(checkOutputErrorsIncremental); + + function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray); + + const outputFile = changeExtension(file.path, ".js"); + assert(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); + } + } + + it("uses watchFile when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses non recursive dynamic polling when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); + }); + + it("when there are symlinks to folders in recursive folders", () => { + const cwd = "/home/user/projects/myproject"; + const file1: File = { + path: `${cwd}/src/file.ts`, + content: `import * as a from "a"` + }; + const tsconfig: File = { + path: `${cwd}/tsconfig.json`, + content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` + }; + const realA: File = { + path: `${cwd}/node_modules/reala/index.d.ts`, + content: `export {}` + }; + const realB: File = { + path: `${cwd}/node_modules/realb/index.d.ts`, + content: `export {}` + }; + const symLinkA: SymLink = { + path: `${cwd}/node_modules/a`, + symLink: `${cwd}/node_modules/reala` + }; + const symLinkB: SymLink = { + path: `${cwd}/node_modules/b`, + symLink: `${cwd}/node_modules/realb` + }; + const symLinkBInA: SymLink = { + path: `${cwd}/node_modules/reala/node_modules/b`, + symLink: `${cwd}/node_modules/b` + }; + const symLinkAInB: SymLink = { + path: `${cwd}/node_modules/realb/node_modules/a`, + symLink: `${cwd}/node_modules/a` + }; + const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, + `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts deleted file mode 100644 index 87e3e736367..00000000000 --- a/src/testRunner/unittests/tscWatchMode.ts +++ /dev/null @@ -1,3276 +0,0 @@ -namespace ts.tscWatch { - export import WatchedSystem = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; - export import checkArray = TestFSWithWatch.checkArray; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - export import checkOutputContains = TestFSWithWatch.checkOutputContains; - export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; - export import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - - export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray) { - checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); - } - - export function checkProgramRootFiles(program: Program, expectedFiles: ReadonlyArray) { - checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); - } - - export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram(); - } - - interface Watch { - (): Program; - getBuilderProgram(): EmitAndSemanticDiagnosticsBuilderProgram; - } - - export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - const result = (() => watch.getCurrentProgram().getProgram()) as Watch; - result.getBuilderProgram = () => watch.getCurrentProgram(); - return result; - } - - function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram().getProgram(); - } - - function getEmittedLineForMultiFileOutput(file: File, host: WatchedSystem) { - return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; - } - - function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { - return `TSFILE: ${filename}${host.newLine}`; - } - - interface FileOrFolderEmit extends File { - output?: string; - } - - function getFileOrFolderEmit(file: File, getOutput?: (file: File) => string): FileOrFolderEmit { - const result = file as FileOrFolderEmit; - if (getOutput) { - result.output = getOutput(file); - } - return result; - } - - function getEmittedLines(files: FileOrFolderEmit[]) { - const seen = createMap(); - const result: string[] = []; - for (const { output } of files) { - if (output && !seen.has(output)) { - seen.set(output, true); - result.push(output); - } - } - return result; - } - - function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { - const expectedAffectedFiles = getEmittedLines(affectedFiles); - const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); - checkOutputContains(host, expectedAffectedFiles); - checkOutputDoesNotContain(host, expectedNonAffectedFiles); - } - - const elapsedRegex = /^Elapsed:: [0-9]+ms/; - function checkOutputErrors( - host: WatchedSystem, - logsBeforeWatchDiagnostic: string[] | undefined, - preErrorsWatchDiagnostic: Diagnostic, - logsBeforeErrors: string[] | undefined, - errors: ReadonlyArray | ReadonlyArray, - disableConsoleClears?: boolean | undefined, - ...postErrorsWatchDiagnostics: Diagnostic[] - ) { - let screenClears = 0; - const outputs = host.getOutput(); - const expectedOutputCount = 1 + errors.length + postErrorsWatchDiagnostics.length + - (logsBeforeWatchDiagnostic ? logsBeforeWatchDiagnostic.length : 0) + (logsBeforeErrors ? logsBeforeErrors.length : 0); - assert.equal(outputs.length, expectedOutputCount, JSON.stringify(outputs)); - let index = 0; - forEach(logsBeforeWatchDiagnostic, log => assertLog("logsBeforeWatchDiagnostic", log)); - assertWatchDiagnostic(preErrorsWatchDiagnostic); - forEach(logsBeforeErrors, log => assertLog("logBeforeError", log)); - // Verify errors - forEach(errors, assertDiagnostic); - forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); - assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); - host.clearOutput(); - - function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { - return !!(diagnostic as Diagnostic).messageText; - } - - function assertDiagnostic(diagnostic: Diagnostic | string) { - const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; - assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); - index++; - } - - function assertLog(caption: string, expected: string) { - const actual = outputs[index]; - assert.equal(actual.replace(elapsedRegex, ""), expected.replace(elapsedRegex, ""), getOutputAtFailedMessage(caption, expected)); - index++; - } - - function assertWatchDiagnostic(diagnostic: Diagnostic) { - const expected = getWatchDiagnosticWithoutDate(diagnostic); - if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { - assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); - screenClears++; - } - assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); - index++; - } - - function getOutputAtFailedMessage(caption: string, expectedOutput: string) { - return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; - } - - function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { - const newLines = contains(screenStartingMessageCodes, diagnostic.code) - ? `${host.newLine}${host.newLine}` - : host.newLine; - return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; - } - } - - function createErrorsFoundCompilerDiagnostic(errors: ReadonlyArray | ReadonlyArray) { - return errors.length === 1 - ? createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) - : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); - } - - export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - /*logsBeforeWatchDiagnostic*/ undefined, - createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode), - logsBeforeErrors, - errors, - disableConsoleClears, - createErrorsFoundCompilerDiagnostic(errors)); - } - - export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - logsBeforeWatchDiagnostic, - createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), - logsBeforeErrors, - errors, - disableConsoleClears, - createErrorsFoundCompilerDiagnostic(errors)); - } - - function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray | ReadonlyArray, expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - logsBeforeWatchDiagnostic, - createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), - logsBeforeErrors, - errors, - disableConsoleClears); - assert.equal(host.exitCode, expectedExitCode); - } - - function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain { - return !!(message as DiagnosticMessageChain).messageText; - } - function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ..._args: (string | number)[]): Diagnostic { - let text: DiagnosticMessageChain | string; - if (isDiagnosticMessageChain(message)) { - text = message; - } - else { - text = getLocaleSpecificMessage(message); - - if (arguments.length > 4) { - text = formatStringFromArgs(text, arguments, 4); - } - } - - return { - file, - start, - length, - - messageText: text, - category: message.category, - code: message.code, - }; - } - - function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { - return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); - } - - function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { - return getDiagnosticOfFileFrom(file, start, length, message, ...args); - } - - function getUnknownCompilerOption(program: Program, configFile: File, option: string) { - const quotedOption = `"${option}"`; - return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); - } - - function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { - return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase()))!, - start, length, message, ...args); - } - - function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { - const quotedModuleName = `"${moduleName}"`; - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); - } - - describe("tsc-watch program updates", () => { - const commonFile1: File = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - const commonFile2: File = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; - - it("create watch without config file", () => { - const appFile: File = { - path: "/a/b/c/app.ts", - content: ` - import {f} from "./module" - console.log(f) - ` - }; - - const moduleFile: File = { - path: "/a/b/c/module.d.ts", - content: `export let x: number` - }; - const host = createWatchedSystem([appFile, moduleFile, libFile]); - const watch = createWatchOfFilesAndCompilerOptions([appFile.path], host); - - checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); - - // TODO: Should we watch creation of config files in the root file's file hierarchy? - - // const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; - // const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - // checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); - }); - - it("can handle tsconfig file name with difference casing", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - include: ["app.ts"] - }) - }; - - const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); - const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); - const watch = createWatchOfConfigFile(upperCaseConfigFilePath, host); - checkProgramActualFiles(watch(), [combinePaths(getDirectoryPath(upperCaseConfigFilePath), getBaseFileName(f1.path))]); - }); - - it("create configured project without file list", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: ` - { - "compilerOptions": {}, - "exclude": [ - "e" - ] - }` - }; - const file1: File = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: File = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: File = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - - const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); - const watch = createWatchProgram(createWatchCompilerHostOfConfigFile(configFile.path, {}, host, /*createProgram*/ undefined, notImplemented)); - - checkProgramActualFiles(watch.getCurrentProgram().getProgram(), [file1.path, libFile.path, file2.path]); - checkProgramRootFiles(watch.getCurrentProgram().getProgram(), [file1.path, file2.path]); - checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); - const configDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - }); - - // TODO: if watching for config file creation - // it("add and then remove a config file in a folder with loose files", () => { - // }); - - it("add new files to a configured program without file list", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createWatchedSystem([commonFile1, libFile, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - const configDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); - - checkProgramRootFiles(watch(), [commonFile1.path]); - - // add a new ts file - host.reloadFS([commonFile1, commonFile2, libFile, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("should ignore non-existing files specified in the config file", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": [ - "commonFile1.ts", - "commonFile3.ts" - ] - }` - }; - const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - - const commonFile3 = "/a/b/commonFile3.ts"; - checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); - checkProgramActualFiles(watch(), [commonFile1.path]); - }); - - it("handle recreated files correctly", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - - // delete commonFile2 - host.reloadFS([commonFile1, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [commonFile1.path]); - - // re-add commonFile2 - host.reloadFS([commonFile1, commonFile2, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("handles the missing files - that were added to program because they were added with /// { - const commonFile2Name = "commonFile2.ts"; - const file1: File = { - path: "/a/b/commonFile1.ts", - content: `/// - let x = y` - }; - const host = createWatchedSystem([file1, libFile]); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, libFile.path]); - checkOutputErrorsInitial(host, [ - getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf(commonFile2Name), commonFile2Name.length, Diagnostics.File_0_not_found, commonFile2.path), - getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf("y"), 1, Diagnostics.Cannot_find_name_0, "y") - ]); - - host.reloadFS([file1, commonFile2, libFile]); - host.runQueuedTimeoutCallbacks(); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("should reflect change in config file", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}", "${commonFile2.path}"] - }` - }; - const files = [commonFile1, commonFile2, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(configFile.path, host); - - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - configFile.content = `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}"] - }`; - - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); // reload the configured project - checkProgramRootFiles(watch(), [commonFile1.path]); - }); - - it("works correctly when config file is changed but its content havent", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "files": ["${commonFile1.path}", "${commonFile2.path}"] - }` - }; - const files = [libFile, commonFile1, commonFile2, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(configFile.path, host); - - checkProgramActualFiles(watch(), [libFile.path, commonFile1.path, commonFile2.path]); - checkOutputErrorsInitial(host, emptyArray); - - host.modifyFile(configFile.path, configFile.content); - host.checkTimeoutQueueLengthAndRun(1); // reload the configured project - - checkProgramActualFiles(watch(), [libFile.path, commonFile1.path, commonFile2.path]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("Updates diagnostics when '--noUnusedLabels' changes", () => { - const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; - const files = [libFile, aTs]; - const paths = files.map(f => f.path); - const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; - const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; - - const host = createWatchedSystem([...files, tsconfig]); - const watch = createWatchOfConfigFile(tsconfig.path, host); - - checkProgramActualFiles(watch(), paths); - checkOutputErrorsInitial(host, emptyArray); - - host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); - host.checkTimeoutQueueLengthAndRun(1); // reload the configured project - - checkProgramActualFiles(watch(), paths); - checkOutputErrorsIncremental(host, [ - getDiagnosticOfFileFromProgram(watch(), aTs.path, 0, "label".length, Diagnostics.Unused_label), - ]); - - host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ true)); - host.checkTimeoutQueueLengthAndRun(1); // reload the configured project - checkProgramActualFiles(watch(), paths); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("files explicitly excluded in config file", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "exclude": ["/a/c"] - }` - }; - const excludedFile1: File = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; - - const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("should properly handle module resolution changes in config file", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: File = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: File = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "moduleResolution": "node" - }, - "files": ["${file1.path}"] - }` - }; - const files = [file1, nodeModuleFile, classicModuleFile, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); - - configFile.content = `{ - "compilerOptions": { - "moduleResolution": "classic" - }, - "files": ["${file1.path}"] - }`; - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, classicModuleFile.path]); - }); - - it("should tolerate config file errors and still try to build a project", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "target": "es6", - "allowAnything": true - }, - "someOtherProperty": {} - }` - }; - const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); - }); - - it("changes in files are reflected in project structure", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export let x = 1` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - - const modifiedFile2 = { - path: file2.path, - content: `export * from "../c/f3"` // now inferred project should inclule file3 - }; - - host.reloadFS([file1, modifiedFile2, file3]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path]); - checkProgramActualFiles(watch(), [file1.path, modifiedFile2.path, file3.path]); - }); - - it("deleted files affect project structure", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); - - host.reloadFS([file1, file3]); - host.checkTimeoutQueueLengthAndRun(1); - - checkProgramActualFiles(watch(), [file1.path]); - }); - - it("deleted files affect project structure - 2", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchOfFilesAndCompilerOptions([file1.path, file3.path], host); - checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); - - host.reloadFS([file1, file3]); - host.checkTimeoutQueueLengthAndRun(1); - - checkProgramActualFiles(watch(), [file1.path, file3.path]); - }); - - it("config file includes the file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export let x = 5" - }; - const file2 = { - path: "/a/c/f2.ts", - content: `import {x} from "../b/f1"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: "export let y = 1" - }; - const configFile = { - path: "/a/c/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) - }; - - const host = createWatchedSystem([file1, file2, file3, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - - checkProgramRootFiles(watch(), [file2.path, file3.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); - }); - - it("correctly migrate files between projects", () => { - const file1 = { - path: "/a/b/f1.ts", - content: ` - export * from "../c/f2"; - export * from "../d/f3";` - }; - const file2 = { - path: "/a/c/f2.ts", - content: "export let x = 1;" - }; - const file3 = { - path: "/a/d/f3.ts", - content: "export let y = 1;" - }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchOfFilesAndCompilerOptions([file2.path, file3.path], host); - checkProgramActualFiles(watch(), [file2.path, file3.path]); - - const watch2 = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); - - // Previous program shouldnt be updated - checkProgramActualFiles(watch(), [file2.path, file3.path]); - host.checkTimeoutQueueLength(0); - }); - - it("can correctly update configured project when set of root files has changed (new file on disk)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - - const host = createWatchedSystem([file1, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), [file1.path]); - - host.reloadFS([file1, file2, configFile]); - host.checkTimeoutQueueLengthAndRun(1); - - checkProgramActualFiles(watch(), [file1.path, file2.path]); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - }); - - it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) - }; - - const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - - checkProgramActualFiles(watch(), [file1.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - }); - - it("can update configured project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramRootFiles(watch(), [file1.path, file2.path]); - checkProgramActualFiles(watch(), [file1.path, file2.path]); - }); - - it("config file is deleted", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 2;" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const host = createWatchedSystem([file1, file2, libFile, config]); - const watch = createWatchOfConfigFile(config.path, host); - - checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); - checkOutputErrorsInitial(host, emptyArray); - - host.reloadFS([file1, file2, libFile]); - host.checkTimeoutQueueLengthAndRun(1); - - checkOutputErrorsIncrementalWithExit(host, [ - getDiagnosticWithoutFile(Diagnostics.File_0_not_found, config.path) - ], ExitStatus.DiagnosticsPresent_OutputsSkipped); - }); - - it("Proper errors: document is not contained in project", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const corruptedConfig = { - path: "/a/b/tsconfig.json", - content: "{" - }; - const host = createWatchedSystem([file1, corruptedConfig]); - const watch = createWatchOfConfigFile(corruptedConfig.path, host); - - checkProgramActualFiles(watch(), [file1.path]); - }); - - it("correctly handles changes in lib section of config file", () => { - const libES5 = { - path: "/compiler/lib.es5.d.ts", - content: "declare const eval: any" - }; - const libES2015Promise = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" - }; - const app = { - path: "/src/app.ts", - content: "var x: Promise;" - }; - const config1 = { - path: "/src/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5" - ] - } - }) - }; - const config2 = { - path: config1.path, - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5", - "es2015.promise" - ] - } - }) - }; - const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - const watch = createWatchOfConfigFile(config1.path, host); - - checkProgramActualFiles(watch(), [libES5.path, app.path]); - - host.reloadFS([libES5, libES2015Promise, app, config2]); - host.checkTimeoutQueueLengthAndRun(1); - checkProgramActualFiles(watch(), [libES5.path, libES2015Promise.path, app.path]); - }); - - it("should handle non-existing directories in config file", () => { - const f = { - path: "/a/src/app.ts", - content: "let x = 1;" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {}, - include: [ - "src/**/*", - "notexistingfolder/*" - ] - }) - }; - const host = createWatchedSystem([f, config]); - const watch = createWatchOfConfigFile(config.path, host); - checkProgramActualFiles(watch(), [f.path]); - }); - - it("rename a module file and rename back should restore the states for inferred projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - const host = createWatchedSystem([moduleFile, file1, libFile]); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkOutputErrorsInitial(host, emptyArray); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ]); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("rename a module file and rename back should restore the states for configured projects", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkOutputErrorsInitial(host, emptyArray); - - const moduleFileOldPath = moduleFile.path; - const moduleFileNewPath = "/a/b/moduleFile1.ts"; - moduleFile.path = moduleFileNewPath; - host.reloadFS([moduleFile, file1, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ]); - - moduleFile.path = moduleFileOldPath; - host.reloadFS([moduleFile, file1, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("types should load from config file path if config exists", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) - }; - const node = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: "declare var process: any" - }; - const cwd = { - path: "/a/c" - }; - const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); - const watch = createWatchOfConfigFile(config.path, host); - - checkProgramActualFiles(watch(), [f1.path, node.path]); - }); - - it("add the missing module file for inferred project: should remove the `module not found` error", () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - const host = createWatchedSystem([file1, libFile]); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ]); - - host.reloadFS([file1, moduleFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("Configure file diagnostics events are generated when the config file has errors", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": { - "foo": "bar", - "allowJS": true - } - }` - }; - - const host = createWatchedSystem([file, configFile, libFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkOutputErrorsInitial(host, [ - getUnknownCompilerOption(watch(), configFile, "foo"), - getUnknownCompilerOption(watch(), configFile, "allowJS") - ]); - }); - - it("If config file doesnt have errors, they are not reported", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {} - }` - }; - - const host = createWatchedSystem([file, configFile, libFile]); - createWatchOfConfigFile(configFile.path, host); - checkOutputErrorsInitial(host, emptyArray); - }); - - it("Reports errors when the config file changes", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {} - }` - }; - - const host = createWatchedSystem([file, configFile, libFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - checkOutputErrorsInitial(host, emptyArray); - - configFile.content = `{ - "compilerOptions": { - "haha": 123 - } - }`; - host.reloadFS([file, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, [ - getUnknownCompilerOption(watch(), configFile, "haha") - ]); - - configFile.content = `{ - "compilerOptions": {} - }`; - host.reloadFS([file, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ - "compilerOptions": {}, - "include": ["app/*", "test/**/*", "something"] - }` - }; - const file1 = { - path: "/a/b/file1.ts", - content: "let t = 10;" - }; - - const host = createWatchedSystem([file1, configFile, libFile]); - const watch = createWatchOfConfigFile(configFile.path, host); - - checkProgramActualFiles(watch(), emptyArray); - checkOutputErrorsInitial(host, [ - "error TS18003: No inputs were found in config file '/a/b/tsconfig.json'. Specified 'include' paths were '[\"app/*\",\"test/**/*\",\"something\"]' and 'exclude' paths were '[]'.\n" - ]); - }); - - it("non-existing directories listed in config file input array should be able to handle @types if input file list is empty", () => { - const f = { - path: "/a/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compiler: {}, - files: [] - }) - }; - const t1 = { - path: "/a/node_modules/@types/typings/index.d.ts", - content: `export * from "./lib"` - }; - const t2 = { - path: "/a/node_modules/@types/typings/lib.d.ts", - content: `export const x: number` - }; - const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); - const watch = createWatchOfConfigFile(config.path, host); - - checkProgramActualFiles(watch(), emptyArray); - checkOutputErrorsInitial(host, [ - "tsconfig.json(1,24): error TS18002: The 'files' list in config file '/a/tsconfig.json' is empty.\n" - ]); - }); - - it("should support files without extensions", () => { - const f = { - path: "/a/compile", - content: "let x = 1" - }; - const host = createWatchedSystem([f, libFile]); - const watch = createWatchOfFilesAndCompilerOptions([f.path], host, { allowNonTsExtensions: true }); - checkProgramActualFiles(watch(), [f.path, libFile.path]); - }); - - it("Options Diagnostic locations reported correctly with changes in configFile contents when options change", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFileContentBeforeComment = `{`; - const configFileContentComment = ` - // comment - // More comment`; - const configFileContentAfterComment = ` - "compilerOptions": { - "allowJs": true, - "declaration": true - } - }`; - const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; - const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; - const configFile = { - path: "/a/b/tsconfig.json", - content: configFileContentWithComment - }; - - const files = [file, libFile, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(configFile.path, host); - const errors = () => [ - getDiagnosticOfFile(watch().getCompilerOptions().configFile!, configFile.content.indexOf('"allowJs"'), '"allowJs"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration"), - getDiagnosticOfFile(watch().getCompilerOptions().configFile!, configFile.content.indexOf('"declaration"'), '"declaration"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration") - ]; - const intialErrors = errors(); - checkOutputErrorsInitial(host, intialErrors); - - configFile.content = configFileContentWithoutCommentLine; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - const nowErrors = errors(); - checkOutputErrorsIncremental(host, nowErrors); - assert.equal(nowErrors[0].start, intialErrors[0].start! - configFileContentComment.length); - assert.equal(nowErrors[1].start, intialErrors[1].start! - configFileContentComment.length); - }); - - describe("should not trigger should not trigger recompilation because of program emit", () => { - function verifyWithOptions(options: CompilerOptions, outputFiles: ReadonlyArray) { - const proj = "/user/username/projects/myproject"; - const file1: File = { - path: `${proj}/file1.ts`, - content: "export const c = 30;" - }; - const file2: File = { - path: `${proj}/src/file2.ts`, - content: `import {c} from "file1"; export const d = 30;` - }; - const tsconfig: File = { - path: `${proj}/tsconfig.json`, - content: generateTSConfig(options, emptyArray, "\n") - }; - const host = createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: proj }); - const watch = createWatchOfConfigFile(tsconfig.path, host, /*maxNumberOfFilesToIterateForInvalidation*/1); - checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); - - outputFiles.forEach(f => host.fileExists(f)); - - // This should be 0 - host.checkTimeoutQueueLengthAndRun(0); - } - - it("without outDir or outFile is specified", () => { - verifyWithOptions({ module: ModuleKind.AMD }, ["file1.js", "src/file2.js"]); - }); - - it("with outFile", () => { - verifyWithOptions({ module: ModuleKind.AMD, outFile: "build/outFile.js" }, ["build/outFile.js"]); - }); - - it("when outDir is specified", () => { - verifyWithOptions({ module: ModuleKind.AMD, outDir: "build" }, ["build/file1.js", "build/src/file2.js"]); - }); - - it("when outDir and declarationDir is specified", () => { - verifyWithOptions({ module: ModuleKind.AMD, outDir: "build", declaration: true, declarationDir: "decls" }, - ["build/file1.js", "build/src/file2.js", "decls/file1.d.ts", "decls/src/file2.d.ts"]); - }); - - it("declarationDir is specified", () => { - verifyWithOptions({ module: ModuleKind.AMD, declaration: true, declarationDir: "decls" }, - ["file1.js", "src/file2.js", "decls/file1.d.ts", "decls/src/file2.d.ts"]); - }); - }); - - it("shouldnt report error about unused function incorrectly when file changes from global to module", () => { - const getFileContent = (asModule: boolean) => ` - function one() {} - ${asModule ? "export " : ""}function two() { - return function three() { - one(); - } - }`; - const file: File = { - path: "/a/b/file.ts", - content: getFileContent(/*asModule*/ false) - }; - const files = [file, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([file.path], host, { - noUnusedLocals: true - }); - checkProgramActualFiles(watch(), files.map(file => file.path)); - checkOutputErrorsInitial(host, []); - - file.content = getFileContent(/*asModule*/ true); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), files.map(file => file.path)); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("watched files when file is deleted and new file is added as part of change", () => { - const projectLocation = "/home/username/project"; - const file: File = { - path: `${projectLocation}/src/file1.ts`, - content: "var a = 10;" - }; - const configFile: File = { - path: `${projectLocation}/tsconfig.json`, - content: "{}" - }; - const files = [file, libFile, configFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(configFile.path, host); - verifyProgram(); - - file.path = file.path.replace("file1", "file2"); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyProgram(); - - function verifyProgram() { - checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, [projectLocation, `${projectLocation}/node_modules/@types`], /*recursive*/ true); - checkWatchedFiles(host, files.map(f => f.path)); - } - }); - - it("updates errors correctly when declaration emit is disabled in compiler options", () => { - const currentDirectory = "/user/username/projects/myproject"; - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `import test from './b'; -test(4, 5);` - }; - const bFileContent = `function test(x: number, y: number) { - return x + y / 5; -} -export default test;`; - const bFile: File = { - path: `${currentDirectory}/b.ts`, - content: bFileContent - }; - const tsconfigFile: File = { - path: `${currentDirectory}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "commonjs", - noEmit: true, - strict: true, - } - }) - }; - const files = [aFile, bFile, libFile, tsconfigFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfConfigFile("tsconfig.json", host); - checkOutputErrorsInitial(host, emptyArray); - - changeParameterType("x", "string", [ - getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.indexOf("4"), 1, Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1, "4", "string") - ]); - changeParameterType("y", "string", [ - getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.indexOf("5"), 1, Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1, "5", "string"), - getDiagnosticOfFileFromProgram(watch(), bFile.path, bFile.content.indexOf("y /"), 1, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type) - ]); - - function changeParameterType(parameterName: string, toType: string, expectedErrors: ReadonlyArray) { - const newContent = bFileContent.replace(new RegExp(`${parameterName}\: [a-z]*`), `${parameterName}: ${toType}`); - - verifyErrorsWithBFileContents(newContent, expectedErrors); - verifyErrorsWithBFileContents(bFileContent, emptyArray); - } - - function verifyErrorsWithBFileContents(content: string, expectedErrors: ReadonlyArray) { - host.writeFile(bFile.path, content); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, expectedErrors); - } - }); - - it("updates errors when strictNullChecks changes", () => { - const currentDirectory = "/user/username/projects/myproject"; - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `declare function foo(): null | { hello: any }; -foo().hello` - }; - const config: File = { - path: `${currentDirectory}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - const files = [aFile, config, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfConfigFile("tsconfig.json", host); - checkProgramActualFiles(watch(), [aFile.path, libFile.path]); - checkOutputErrorsInitial(host, emptyArray); - const modifiedTimeOfAJs = host.getModifiedTime(`${currentDirectory}/a.js`); - host.writeFile(config.path, JSON.stringify({ compilerOptions: { strictNullChecks: true } })); - host.runQueuedTimeoutCallbacks(); - const expectedStrictNullErrors = [ - getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.lastIndexOf("foo()"), 5, Diagnostics.Object_is_possibly_null) - ]; - checkOutputErrorsIncremental(host, expectedStrictNullErrors); - // File a need not be rewritten - assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); - host.writeFile(config.path, JSON.stringify({ compilerOptions: { strict: true, alwaysStrict: false } })); // Avoid changing 'alwaysStrict' or must re-bind - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, expectedStrictNullErrors); - // File a need not be rewritten - assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); - host.writeFile(config.path, JSON.stringify({ compilerOptions: {} })); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - // File a need not be rewritten - assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); - }); - - it("updates errors when ambient modules of program changes", () => { - const currentDirectory = "/user/username/projects/myproject"; - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `declare module 'a' { - type foo = number; -}` - }; - const config: File = { - path: `${currentDirectory}/tsconfig.json`, - content: "{}" - }; - const files = [aFile, config, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfConfigFile("tsconfig.json", host); - checkProgramActualFiles(watch(), [aFile.path, libFile.path]); - checkOutputErrorsInitial(host, emptyArray); - - // Create bts with same file contents - const bTsPath = `${currentDirectory}/b.ts`; - host.writeFile(bTsPath, aFile.content); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [aFile.path, "b.ts", libFile.path]); - checkOutputErrorsIncremental(host, [ - "a.ts(2,8): error TS2300: Duplicate identifier 'foo'.\n", - "b.ts(2,8): error TS2300: Duplicate identifier 'foo'.\n" - ]); - - // Delete bTs - host.deleteFile(bTsPath); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [aFile.path, libFile.path]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - describe("updates errors in lib file", () => { - const currentDirectory = "/user/username/projects/myproject"; - const field = "fullscreen"; - const fieldWithoutReadonly = `interface Document { - ${field}: boolean; -}`; - - const libFileWithDocument: File = { - path: libFile.path, - content: `${libFile.content} -interface Document { - readonly ${field}: boolean; -}` - }; - - function getDiagnostic(program: Program, file: File) { - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(field), field.length, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, field); - } - - function verifyLibFileErrorsWith(aFile: File) { - const files = [aFile, libFileWithDocument]; - - function verifyLibErrors(options: CompilerOptions) { - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfFilesAndCompilerOptions([aFile.path], host, options); - checkProgramActualFiles(watch(), [aFile.path, libFile.path]); - checkOutputErrorsInitial(host, getErrors()); - - host.writeFile(aFile.path, aFile.content.replace(fieldWithoutReadonly, "var x: string;")); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [aFile.path, libFile.path]); - checkOutputErrorsIncremental(host, emptyArray); - - host.writeFile(aFile.path, aFile.content); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [aFile.path, libFile.path]); - checkOutputErrorsIncremental(host, getErrors()); - - function getErrors() { - return [ - ...(options.skipLibCheck || options.skipDefaultLibCheck ? [] : [getDiagnostic(watch(), libFileWithDocument)]), - getDiagnostic(watch(), aFile) - ]; - } - } - - it("with default options", () => { - verifyLibErrors({}); - }); - it("with skipLibCheck", () => { - verifyLibErrors({ skipLibCheck: true }); - }); - it("with skipDefaultLibCheck", () => { - verifyLibErrors({ skipDefaultLibCheck: true }); - }); - } - - describe("when non module file changes", () => { - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `${fieldWithoutReadonly} -var y: number;` - }; - verifyLibFileErrorsWith(aFile); - }); - - describe("when module file with global definitions changes", () => { - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `export {} -declare global { -${fieldWithoutReadonly} -var y: number; -}` - }; - verifyLibFileErrorsWith(aFile); - }); - }); - - it("when skipLibCheck and skipDefaultLibCheck changes", () => { - const currentDirectory = "/user/username/projects/myproject"; - const field = "fullscreen"; - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `interface Document { - ${field}: boolean; -}` - }; - const bFile: File = { - path: `${currentDirectory}/b.d.ts`, - content: `interface Document { - ${field}: boolean; -}` - }; - const libFileWithDocument: File = { - path: libFile.path, - content: `${libFile.content} -interface Document { - readonly ${field}: boolean; -}` - }; - const configFile: File = { - path: `${currentDirectory}/tsconfig.json`, - content: "{}" - }; - - const files = [aFile, bFile, configFile, libFileWithDocument]; - - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfConfigFile("tsconfig.json", host); - verifyProgramFiles(); - checkOutputErrorsInitial(host, [ - getDiagnostic(libFileWithDocument), - getDiagnostic(aFile), - getDiagnostic(bFile) - ]); - - verifyConfigChange({ skipLibCheck: true }, [aFile]); - verifyConfigChange({ skipDefaultLibCheck: true }, [aFile, bFile]); - verifyConfigChange({}, [libFileWithDocument, aFile, bFile]); - verifyConfigChange({ skipDefaultLibCheck: true }, [aFile, bFile]); - verifyConfigChange({ skipLibCheck: true }, [aFile]); - verifyConfigChange({}, [libFileWithDocument, aFile, bFile]); - - function verifyConfigChange(compilerOptions: CompilerOptions, errorInFiles: ReadonlyArray) { - host.writeFile(configFile.path, JSON.stringify({ compilerOptions })); - host.runQueuedTimeoutCallbacks(); - verifyProgramFiles(); - checkOutputErrorsIncremental(host, errorInFiles.map(getDiagnostic)); - } - - function getDiagnostic(file: File) { - return getDiagnosticOfFileFromProgram(watch(), file.path, file.content.indexOf(field), field.length, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, field); - } - - function verifyProgramFiles() { - checkProgramActualFiles(watch(), [aFile.path, bFile.path, libFile.path]); - } - }); - }); - - describe("Emit times and Error updates in builder after program changes", () => { - const currentDirectory = "/user/username/projects/myproject"; - const config: File = { - path: `${currentDirectory}/tsconfig.json`, - content: `{}` - }; - function getOutputFileStampAndError(host: WatchedSystem, watch: Watch, file: File) { - const builderProgram = watch.getBuilderProgram(); - const state = builderProgram.getState(); - return { - file, - fileStamp: host.getModifiedTime(file.path.replace(".ts", ".js")), - errors: builderProgram.getSemanticDiagnostics(watch().getSourceFileByPath(file.path as Path)), - errorsFromOldState: !!state.semanticDiagnosticsFromOldState && state.semanticDiagnosticsFromOldState.has(file.path) - }; - } - - function getOutputFileStampsAndErrors(host: WatchedSystem, watch: Watch, directoryFiles: ReadonlyArray) { - return directoryFiles.map(d => getOutputFileStampAndError(host, watch, d)); - } - - function findStampAndErrors(stampsAndErrors: ReadonlyArray>, file: File) { - return find(stampsAndErrors, info => info.file === file)!; - } - - function verifyOutputFileStampsAndErrors( - file: File, - emitExpected: boolean, - errorRefershExpected: boolean, - beforeChangeFileStampsAndErrors: ReadonlyArray>, - afterChangeFileStampsAndErrors: ReadonlyArray> - ) { - const beforeChange = findStampAndErrors(beforeChangeFileStampsAndErrors, file); - const afterChange = findStampAndErrors(afterChangeFileStampsAndErrors, file); - if (emitExpected) { - assert.notStrictEqual(afterChange.fileStamp, beforeChange.fileStamp, `Expected emit for file ${file.path}`); - } - else { - assert.strictEqual(afterChange.fileStamp, beforeChange.fileStamp, `Did not expect new emit for file ${file.path}`); - } - if (errorRefershExpected) { - if (afterChange.errors !== emptyArray || beforeChange.errors !== emptyArray) { - assert.notStrictEqual(afterChange.errors, beforeChange.errors, `Expected new errors for file ${file.path}`); - } - assert.isFalse(afterChange.errorsFromOldState, `Expected errors to be not copied from old state for file ${file.path}`); - } - else { - assert.strictEqual(afterChange.errors, beforeChange.errors, `Expected errors to not change for file ${file.path}`); - assert.isTrue(afterChange.errorsFromOldState, `Expected errors to be copied from old state for file ${file.path}`); - } - } - - interface VerifyEmitAndErrorUpdates { - change: (host: WatchedSystem) => void; - getInitialErrors: (watch: Watch) => ReadonlyArray | ReadonlyArray; - getIncrementalErrors: (watch: Watch) => ReadonlyArray | ReadonlyArray; - filesWithNewEmit: ReadonlyArray; - filesWithOnlyErrorRefresh: ReadonlyArray; - filesNotTouched: ReadonlyArray; - configFile?: File; - } - - function verifyEmitAndErrorUpdates({ filesWithNewEmit, filesWithOnlyErrorRefresh, filesNotTouched, configFile = config, change, getInitialErrors, getIncrementalErrors }: VerifyEmitAndErrorUpdates) { - const nonLibFiles = [...filesWithNewEmit, ...filesWithOnlyErrorRefresh, ...filesNotTouched]; - const files = [...nonLibFiles, configFile, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfConfigFile("tsconfig.json", host); - checkProgramActualFiles(watch(), [...nonLibFiles.map(f => f.path), libFile.path]); - checkOutputErrorsInitial(host, getInitialErrors(watch)); - const beforeChange = getOutputFileStampsAndErrors(host, watch, nonLibFiles); - change(host); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, getIncrementalErrors(watch)); - const afterChange = getOutputFileStampsAndErrors(host, watch, nonLibFiles); - filesWithNewEmit.forEach(file => verifyOutputFileStampsAndErrors(file, /*emitExpected*/ true, /*errorRefershExpected*/ true, beforeChange, afterChange)); - filesWithOnlyErrorRefresh.forEach(file => verifyOutputFileStampsAndErrors(file, /*emitExpected*/ false, /*errorRefershExpected*/ true, beforeChange, afterChange)); - filesNotTouched.forEach(file => verifyOutputFileStampsAndErrors(file, /*emitExpected*/ false, /*errorRefershExpected*/ false, beforeChange, afterChange)); - } - - describe("deep import changes", () => { - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `import {B} from './b'; -declare var console: any; -let b = new B(); -console.log(b.c.d);` - }; - - function verifyDeepImportChange(bFile: File, cFile: File) { - const filesWithNewEmit: File[] = []; - const filesWithOnlyErrorRefresh = [aFile]; - addImportedModule(bFile); - addImportedModule(cFile); - verifyEmitAndErrorUpdates({ - filesWithNewEmit, - filesWithOnlyErrorRefresh, - filesNotTouched: emptyArray, - change: host => host.writeFile(cFile.path, cFile.content.replace("d", "d2")), - getInitialErrors: () => emptyArray, - getIncrementalErrors: watch => [ - getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.lastIndexOf("d"), 1, Diagnostics.Property_0_does_not_exist_on_type_1, "d", "C") - ] - }); - - function addImportedModule(file: File) { - if (file.path.endsWith(".d.ts")) { - filesWithOnlyErrorRefresh.push(file); - } - else { - filesWithNewEmit.push(file); - } - } - } - - it("updates errors when deep import file changes", () => { - const bFile: File = { - path: `${currentDirectory}/b.ts`, - content: `import {C} from './c'; -export class B -{ - c = new C(); -}` - }; - const cFile: File = { - path: `${currentDirectory}/c.ts`, - content: `export class C -{ - d = 1; -}` - }; - verifyDeepImportChange(bFile, cFile); - }); - - it("updates errors when deep import through declaration file changes", () => { - const bFile: File = { - path: `${currentDirectory}/b.d.ts`, - content: `import {C} from './c'; -export class B -{ - c: C; -}` - }; - const cFile: File = { - path: `${currentDirectory}/c.d.ts`, - content: `export class C -{ - d: number; -}` - }; - verifyDeepImportChange(bFile, cFile); - }); - }); - - it("updates errors in file not exporting a deep multilevel import that changes", () => { - const aFile: File = { - path: `${currentDirectory}/a.ts`, - content: `export interface Point { - name: string; - c: Coords; -} -export interface Coords { - x2: number; - y: number; -}` - }; - const bFile: File = { - path: `${currentDirectory}/b.ts`, - content: `import { Point } from "./a"; -export interface PointWrapper extends Point { -}` - }; - const cFile: File = { - path: `${currentDirectory}/c.ts`, - content: `import { PointWrapper } from "./b"; -export function getPoint(): PointWrapper { - return { - name: "test", - c: { - x: 1, - y: 2 - } - } -};` - }; - const dFile: File = { - path: `${currentDirectory}/d.ts`, - content: `import { getPoint } from "./c"; -getPoint().c.x;` - }; - const eFile: File = { - path: `${currentDirectory}/e.ts`, - content: `import "./d";` - }; - verifyEmitAndErrorUpdates({ - filesWithNewEmit: [aFile, bFile], - filesWithOnlyErrorRefresh: [cFile, dFile], - filesNotTouched: [eFile], - change: host => host.writeFile(aFile.path, aFile.content.replace("x2", "x")), - getInitialErrors: watch => [ - getDiagnosticOfFileFromProgram(watch(), cFile.path, cFile.content.indexOf("x: 1"), 4, chainDiagnosticMessages( - chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, "x", "Coords"), - Diagnostics.Type_0_is_not_assignable_to_type_1, - "{ x: number; y: number; }", - "Coords" - )), - getDiagnosticOfFileFromProgram(watch(), dFile.path, dFile.content.lastIndexOf("x"), 1, Diagnostics.Property_0_does_not_exist_on_type_1, "x", "Coords") - ], - getIncrementalErrors: () => emptyArray - }); - }); - - describe("updates errors when file transitively exported file changes", () => { - const config: File = { - path: `${currentDirectory}/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts"], - compilerOptions: { baseUrl: "." } - }) - }; - const app: File = { - path: `${currentDirectory}/app.ts`, - content: `import { Data } from "lib2/public"; -export class App { - public constructor() { - new Data().test(); - } -}` - }; - const lib2Public: File = { - path: `${currentDirectory}/lib2/public.ts`, - content: `export * from "./data";` - }; - const lib2Data: File = { - path: `${currentDirectory}/lib2/data.ts`, - content: `import { ITest } from "lib1/public"; -export class Data { - public test() { - const result: ITest = { - title: "title" - } - return result; - } -}` - }; - const lib1Public: File = { - path: `${currentDirectory}/lib1/public.ts`, - content: `export * from "./tools/public";` - }; - const lib1ToolsPublic: File = { - path: `${currentDirectory}/lib1/tools/public.ts`, - content: `export * from "./tools.interface";` - }; - const lib1ToolsInterface: File = { - path: `${currentDirectory}/lib1/tools/tools.interface.ts`, - content: `export interface ITest { - title: string; -}` - }; - - function verifyTransitiveExports(lib2Data: File, lib2Data2?: File) { - const filesWithNewEmit = [lib1ToolsInterface, lib1ToolsPublic]; - const filesWithOnlyErrorRefresh = [app, lib2Public, lib1Public, lib2Data]; - if (lib2Data2) { - filesWithOnlyErrorRefresh.push(lib2Data2); - } - verifyEmitAndErrorUpdates({ - filesWithNewEmit, - filesWithOnlyErrorRefresh, - filesNotTouched: emptyArray, - configFile: config, - change: host => host.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")), - getInitialErrors: () => emptyArray, - getIncrementalErrors: () => [ - "lib2/data.ts(5,13): error TS2322: Type '{ title: string; }' is not assignable to type 'ITest'.\n Object literal may only specify known properties, but 'title' does not exist in type 'ITest'. Did you mean to write 'title2'?\n" - ] - }); - } - it("when there are no circular import and exports", () => { - verifyTransitiveExports(lib2Data); - }); - - it("when there are circular import and exports", () => { - const lib2Data: File = { - path: `${currentDirectory}/lib2/data.ts`, - content: `import { ITest } from "lib1/public"; import { Data2 } from "./data2"; -export class Data { - public dat?: Data2; public test() { - const result: ITest = { - title: "title" - } - return result; - } -}` - }; - const lib2Data2: File = { - path: `${currentDirectory}/lib2/data2.ts`, - content: `import { Data } from "./data"; -export class Data2 { - public dat?: Data; -}` - }; - verifyTransitiveExports(lib2Data, lib2Data2); - }); - }); - }); - - describe("tsc-watch emit with outFile or out setting", () => { - function createWatchForOut(out?: string, outFile?: string) { - const host = createWatchedSystem([]); - const config: FileOrFolderEmit = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { listEmittedFiles: true } - }) - }; - - let getOutput: (file: File) => string; - if (out) { - config.content = JSON.stringify({ - compilerOptions: { listEmittedFiles: true, out } - }); - getOutput = __ => getEmittedLineForSingleFileOutput(out, host); - } - else if (outFile) { - config.content = JSON.stringify({ - compilerOptions: { listEmittedFiles: true, outFile } - }); - getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host); - } - else { - getOutput = file => getEmittedLineForMultiFileOutput(file, host); - } - - const f1 = getFileOrFolderEmit({ - path: "/a/a.ts", - content: "let x = 1" - }, getOutput); - const f2 = getFileOrFolderEmit({ - path: "/a/b.ts", - content: "let y = 1" - }, getOutput); - - const files = [f1, f2, config, libFile]; - host.reloadFS(files); - createWatchOfConfigFile(config.path, host); - - const allEmittedLines = getEmittedLines(files); - checkOutputContains(host, allEmittedLines); - host.clearOutput(); - - f1.content = "let x = 11"; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkAffectedLines(host, [f1], allEmittedLines); - } - - it("projectUsesOutFile should not be returned if not set", () => { - createWatchForOut(); - }); - - it("projectUsesOutFile should be true if out is set", () => { - const outJs = "/a/out.js"; - createWatchForOut(outJs); - }); - - it("projectUsesOutFile should be true if outFile is set", () => { - const outJs = "/a/out.js"; - createWatchForOut(/*out*/ undefined, outJs); - }); - - function verifyFilesEmittedOnce(useOutFile: boolean) { - const file1: File = { - path: "/a/b/output/AnotherDependency/file1.d.ts", - content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" - }; - const file2: File = { - path: "/a/b/dependencies/file2.d.ts", - content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" - }; - const file3: File = { - path: "/a/b/project/src/main.ts", - content: "namespace Main { export function fooBar() {} }" - }; - const file4: File = { - path: "/a/b/project/src/main2.ts", - content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" - }; - const configFile: File = { - path: "/a/b/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: useOutFile ? - { outFile: "../output/common.js", target: "es5" } : - { outDir: "../output", target: "es5" }, - files: [file1.path, file2.path, file3.path, file4.path] - }) - }; - const files = [file1, file2, file3, file4]; - const allfiles = files.concat(configFile); - const host = createWatchedSystem(allfiles); - const originalWriteFile = host.writeFile.bind(host); - const mapOfFilesWritten = createMap(); - host.writeFile = (p: string, content: string) => { - const count = mapOfFilesWritten.get(p); - mapOfFilesWritten.set(p, count ? count + 1 : 1); - return originalWriteFile(p, content); - }; - createWatchOfConfigFile(configFile.path, host); - if (useOutFile) { - // Only out file - assert.equal(mapOfFilesWritten.size, 1); - } - else { - // main.js and main2.js - assert.equal(mapOfFilesWritten.size, 2); - } - mapOfFilesWritten.forEach((value, key) => { - assert.equal(value, 1, "Key: " + key); - }); - } - - it("with --outFile and multiple declaration files in the program", () => { - verifyFilesEmittedOnce(/*useOutFile*/ true); - }); - - it("without --outFile and multiple declaration files in the program", () => { - verifyFilesEmittedOnce(/*useOutFile*/ false); - }); - }); - - describe("tsc-watch emit for configured projects", () => { - const file1Consumer1Path = "/a/b/file1Consumer1.ts"; - const moduleFile1Path = "/a/b/moduleFile1.ts"; - const configFilePath = "/a/b/tsconfig.json"; - interface InitialStateParams { - /** custom config file options */ - configObj?: any; - /** list of the files that will be emitted for first compilation */ - firstCompilationEmitFiles?: string[]; - /** get the emit file for file - default is multi file emit line */ - getEmitLine?(file: File, host: WatchedSystem): string; - /** Additional files and folders to add */ - getAdditionalFileOrFolder?(): File[]; - /** initial list of files to emit if not the default list */ - firstReloadFileList?: string[]; - } - function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { - const host = createWatchedSystem([]); - const getOutputName = getEmitLine ? (file: File) => getEmitLine(file, host) : - (file: File) => getEmittedLineForMultiFileOutput(file, host); - - const moduleFile1 = getFileOrFolderEmit({ - path: moduleFile1Path, - content: "export function Foo() { };", - }, getOutputName); - - const file1Consumer1 = getFileOrFolderEmit({ - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }, getOutputName); - - const file1Consumer2 = getFileOrFolderEmit({ - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }, getOutputName); - - const moduleFile2 = getFileOrFolderEmit({ - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;`, - }, getOutputName); - - const globalFile3 = getFileOrFolderEmit({ - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }); - - const additionalFiles = getAdditionalFileOrFolder ? - map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) : - []; - - (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; - const configFile = getFileOrFolderEmit({ - path: configFilePath, - content: JSON.stringify(configObj) - }); - - const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; - let allEmittedFiles = getEmittedLines(files); - host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); - - // Initial compile - createWatchOfConfigFile(configFile.path, host); - if (firstCompilationEmitFiles) { - checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); - } - else { - checkOutputContains(host, allEmittedFiles); - } - host.clearOutput(); - - return { - moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, - files, - getFile, - verifyAffectedFiles, - verifyAffectedAllFiles, - getOutputName - }; - - function getFiles(filelist: string[]) { - return map(filelist, getFile); - } - - function getFile(fileName: string) { - return find(files, file => file.path === fileName)!; - } - - function verifyAffectedAllFiles() { - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputContains(host, allEmittedFiles); - host.clearOutput(); - } - - function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) { - if (!filesToReload) { - filesToReload = files; - } - else if (filesToReload.length > files.length) { - allEmittedFiles = getEmittedLines(filesToReload); - } - host.reloadFS(filesToReload); - host.checkTimeoutQueueLengthAndRun(1); - checkAffectedLines(host, expected, allEmittedFiles); - host.clearOutput(); - } - } - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - verifyAffectedFiles - } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` - moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should be up-to-date with the reference map changes", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - verifyAffectedFiles - } = getInitialState(); - - // Change file1Consumer1 content to `export let y = Foo();` - file1Consumer1.content = `export let y = Foo();`; - verifyAffectedFiles([file1Consumer1]); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer2]); - - // Add the import statements back to file1Consumer1 - file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`; - verifyAffectedFiles([file1Consumer1]); - - // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` - moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]); - - // Multiple file edits in one go: - - // Change file1Consumer1 content to `export let y = Foo();` - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - file1Consumer1.content = `export let y = Foo();`; - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - }); - - it("should be up-to-date with deleted files", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - files, - verifyAffectedFiles - } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - - // Delete file1Consumer2 - const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file); - verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad); - }); - - it("should be up-to-date with newly created files", () => { - const { - moduleFile1, file1Consumer1, file1Consumer2, - files, - verifyAffectedFiles, - getOutputName - } = getInitialState(); - - const file1Consumer3 = getFileOrFolderEmit({ - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }, getOutputName); - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); - }); - - it("should detect changes in non-root files", () => { - const { - moduleFile1, file1Consumer1, - verifyAffectedFiles - } = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1]); - - // change file1 internal, and verify only file1 is affected - moduleFile1.content += "var T1: number;"; - verifyAffectedFiles([moduleFile1]); - }); - - it("should return all files if a global file changed shape", () => { - const { - globalFile3, verifyAffectedAllFiles - } = getInitialState(); - - globalFile3.content += "var T2: string;"; - verifyAffectedAllFiles(); - }); - - it("should always return the file itself if '--isolatedModules' is specified", () => { - const { - moduleFile1, verifyAffectedFiles - } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const outFilePath = "/a/b/out.js"; - const { - moduleFile1, verifyAffectedFiles - } = getInitialState({ - configObj: { compilerOptions: { module: "system", outFile: outFilePath } }, - getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host) - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1]); - }); - - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const { - moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile - } = getInitialState({ - getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] - }); - - const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); - file1Consumer1.content += "export var T: number;"; - verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); - - // Doesnt change the shape of file1Consumer1 - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); - - // Change both files before the timeout - file1Consumer1.content += "export var T2: number;"; - moduleFile1.content = `export var T2: number;export function Foo() { };`; - verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]); - }); - - it("should work fine for files with circular references", () => { - // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode - - const file1: File = { - path: "/a/b/file1.ts", - content: ` - /// - export var t1 = 10;` - }; - const file2: File = { - path: "/a/b/file2.ts", - content: ` - /// - export var t2 = 10;` - }; - const { - configFile, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [file1.path, file2.path], - getAdditionalFileOrFolder: () => [file1, file2], - firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath] - }); - const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path); - - file1Emit.content += "export var t3 = 10;"; - verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]); - - }); - - it("should detect removed code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { - configFile, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path], - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath] - }); - - const referenceFile1Emit = getFile(referenceFile1.path); - verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); - }); - - it("should detect non-existing code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` - /// - export var x = Foo();` - }; - const { - configFile, - moduleFile2, - getFile, - verifyAffectedFiles - } = getInitialState({ - firstCompilationEmitFiles: [referenceFile1.path], - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath] - }); - - const referenceFile1Emit = getFile(referenceFile1.path); - referenceFile1Emit.content += "export var yy = Foo();"; - verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); - - // Create module File2 and see both files are saved - verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); - }); - }); - - describe("tsc-watch emit file content", () => { - interface EmittedFile extends File { - shouldBeWritten: boolean; - } - function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { - return map(contents, (content, index) => { - return { - content, - path: changeExtension(files[index].path, Extension.Js), - shouldBeWritten: true - }; - } - ); - } - function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { - for (const { path, content, shouldBeWritten } of emittedFiles) { - if (shouldBeWritten) { - assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`); - assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`); - } - else { - assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); - } - } - } - - function verifyEmittedFileContents(newLine: string, inputFiles: File[], initialEmittedFileContents: string[], - modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: File) { - const host = createWatchedSystem([], { newLine }); - const files = concatenate( - map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))), - configFile ? [libFile, configFile] : [libFile] - ); - const allEmittedFiles = getEmittedLines(files); - host.reloadFS(files); - - // Initial compile - if (configFile) { - createWatchOfConfigFile(configFile.path, host); - } - else { - // First file as the root - createWatchOfFilesAndCompilerOptions([files[0].path], host, { listEmittedFiles: true }); - } - checkOutputContains(host, allEmittedFiles); - - const emittedFiles = getEmittedFiles(files, initialEmittedFileContents); - verifyEmittedFiles(host, emittedFiles); - host.clearOutput(); - - const affectedFiles = modifyFiles(files, emittedFiles); - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(1); - checkAffectedLines(host, affectedFiles, allEmittedFiles); - - verifyEmittedFiles(host, emittedFiles); - } - - function verifyNewLine(newLine: string) { - const lines = ["var x = 1;", "var y = 2;"]; - const fileContent = lines.join(newLine); - const f = { - path: "/a/app.ts", - content: fileContent - }; - - verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content = fileContent + newLine + "var z = 3;"; - emittedFiles[0].content = files[0].content + newLine; - return [files[0]]; - } - } - - it("handles new lines: \\n", () => { - verifyNewLine("\n"); - }); - - it("handles new lines: \\r\\n", () => { - verifyNewLine("\r\n"); - }); - - it("should emit specified file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` - }; - - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; export let y = Foo();` - }; - - const file3 = { - path: "/a/b/f3.ts", - content: `import {y} from "./f2"; let x = y;` - }; - - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) - }; - - verifyEmittedFileContents("\r\n", [file1, file2, file3], [ - `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, - `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, - `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n` - ], modifyFiles, configFile); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content += `export function foo2() { return 2; }`; - emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`; - emittedFiles[2].shouldBeWritten = false; - return files.slice(0, 2); - } - }); - - it("Elides const enums correctly in incremental compilation", () => { - const currentDirectory = "/user/someone/projects/myproject"; - const file1: File = { - path: `${currentDirectory}/file1.ts`, - content: "export const enum E1 { V = 1 }" - }; - const file2: File = { - path: `${currentDirectory}/file2.ts`, - content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` - }; - const file3: File = { - path: `${currentDirectory}/file3.ts`, - content: `import { E2 } from "./file2"; const v: E2 = E2.V;` - }; - const strictAndEsModule = `"use strict";\nexports.__esModule = true;\n`; - verifyEmittedFileContents("\n", [file3, file2, file1], [ - `${strictAndEsModule}var v = 1 /* V */;\n`, - strictAndEsModule, - strictAndEsModule - ], modifyFiles); - - function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { - files[0].content += `function foo2() { return 2; }`; - emittedFiles[0].content += `function foo2() { return 2; }\n`; - emittedFiles[1].shouldBeWritten = false; - emittedFiles[2].shouldBeWritten = false; - return [files[0]]; - } - }); - - it("file is deleted and created as part of change", () => { - const projectLocation = "/home/username/project"; - const file: File = { - path: `${projectLocation}/app/file.ts`, - content: "var a = 10;" - }; - const fileJs = `${projectLocation}/app/file.js`; - const configFile: File = { - path: `${projectLocation}/tsconfig.json`, - content: JSON.stringify({ - include: [ - "app/**/*.ts" - ] - }) - }; - const files = [file, configFile, libFile]; - const host = createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); - createWatchOfConfigFile("tsconfig.json", host); - verifyProgram(); - - file.content += "\nvar b = 10;"; - - host.reloadFS(files, { invokeFileDeleteCreateAsPartInsteadOfChange: true }); - host.runQueuedTimeoutCallbacks(); - verifyProgram(); - - function verifyProgram() { - assert.isTrue(host.fileExists(fileJs)); - assert.equal(host.readFile(fileJs), file.content + "\n"); - } - }); - }); - - describe("tsc-watch module resolution caching", () => { - it("works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; - - const files = [root, imported, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); - const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); - - // ensure that imported file was found - checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); - - const originalFileExists = host.fileExists; - { - const newContent = `import {x} from "f1" - var x: string = 1;`; - root.content = newContent; - host.reloadFS(files); - - // patch fileExists to make sure that disk is not touched - host.fileExists = notImplemented; - - // trigger synchronization to make sure that import will be fetched from the cache - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - f1IsNotModule, - getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), - cannotFindFoo - ]); - } - { - let fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - - root.content = `import {x} from "f2"`; - host.reloadFS(files); - - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ]); - - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; - - const newContent = `import {x} from "f1"`; - root.content = newContent; - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); - assert.isTrue(fileExistsCalled); - } - }); - - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;` - }; - - const files = [root, libFile]; - const host = createWatchedSystem(files); - const originalFileExists = host.fileExists; - - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - root.content = `import {y} from "bar"`; - host.reloadFS(files.concat(imported)); - - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;export const x = 10;` - }; - - const files = [root, libFile]; - const filesWithImported = files.concat(imported); - const host = createWatchedSystem(filesWithImported); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - return originalFileExists.call(host, fileName); - }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, emptyArray); - - fileExistsCalledForBar = false; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - host.reloadFS(filesWithImported); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("works when module resolution changes to ambient module", () => { - const root = { - path: "/a/b/foo.ts", - content: `import * as fs from "fs";` - }; - - const packageJson = { - path: "/a/b/node_modules/@types/node/package.json", - content: ` -{ - "main": "" -} -` - }; - - const nodeType = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -}` - }; - - const files = [root, libFile]; - const filesWithNodeType = files.concat(packageJson, nodeType); - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - host.reloadFS(filesWithNodeType); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when included file with ambient module changes", () => { - const root = { - path: "/a/b/foo.ts", - content: ` -import * as fs from "fs"; -import * as u from "url"; -` - }; - - const file = { - path: "/a/b/bar.d.ts", - content: ` -declare module "url" { - export interface Url { - href?: string; - } -} -` - }; - - const fileContentWithFS = ` -declare module "fs" { - export interface Stats { - isFile(): boolean; - } -} -`; - - const files = [root, file, libFile]; - const host = createWatchedSystem(files, { currentDirectory: "/a/b" }); - - const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); - - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ]); - - file.content += fileContentWithFS; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - }); - - it("works when reusing program with files from external library", () => { - interface ExpectedFile { path: string; isExpectedToEmit?: boolean; content?: string; } - const configDir = "/a/b/projects/myProject/src/"; - const file1: File = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: File = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: File = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: File = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }; - const outDirFolder = "/a/b/projects/myProject/dist/"; - const programFiles = [file1, file2, module1, libFile]; - const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); - const watch = createWatchOfConfigFile(configFile.path, host); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsInitial(host, emptyArray); - const expectedFiles: ExpectedFile[] = [ - createExpectedEmittedFile(file1), - createExpectedEmittedFile(file2), - createExpectedToNotEmitFile("index.js"), - createExpectedToNotEmitFile("src/index.js"), - createExpectedToNotEmitFile("src/file1.js"), - createExpectedToNotEmitFile("src/file2.js"), - createExpectedToNotEmitFile("lib.js"), - createExpectedToNotEmitFile("lib.d.ts") - ]; - verifyExpectedFiles(expectedFiles); - - file1.content += "\n;"; - expectedFiles[0].content += ";\n"; // Only emit file1 with this change - expectedFiles[1].isExpectedToEmit = false; - host.reloadFS(programFiles.concat(configFile)); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrorsIncremental(host, emptyArray); - verifyExpectedFiles(expectedFiles); - - - function verifyExpectedFiles(expectedFiles: ExpectedFile[]) { - forEach(expectedFiles, f => { - assert.equal(!!host.fileExists(f.path), f.isExpectedToEmit, "File " + f.path + " is expected to " + (f.isExpectedToEmit ? "emit" : "not emit")); - if (f.isExpectedToEmit) { - assert.equal(host.readFile(f.path), f.content, "Expected contents of " + f.path); - } - }); - } - - function createExpectedToNotEmitFile(fileName: string): ExpectedFile { - return { - path: outDirFolder + fileName, - isExpectedToEmit: false - }; - } - - function createExpectedEmittedFile(file: File): ExpectedFile { - return { - path: removeFileExtension(file.path.replace(configDir, outDirFolder)) + Extension.Js, - isExpectedToEmit: true, - content: '"use strict";\nexports.__esModule = true;\n' + file.content.replace("import", "var") + "\n" - }; - } - }); - - it("works when renaming node_modules folder that already contains @types folder", () => { - const currentDirectory = "/user/username/projects/myproject"; - const file: File = { - path: `${currentDirectory}/a.ts`, - content: `import * as q from "qqq";` - }; - const module: File = { - path: `${currentDirectory}/node_modules2/@types/qqq/index.d.ts`, - content: "export {}" - }; - const files = [file, module, libFile]; - const host = createWatchedSystem(files, { currentDirectory }); - const watch = createWatchOfFilesAndCompilerOptions([file.path], host); - - checkProgramActualFiles(watch(), [file.path, libFile.path]); - checkOutputErrorsInitial(host, [getDiagnosticModuleNotFoundOfFile(watch(), file, "qqq")]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, [`${currentDirectory}/node_modules`, `${currentDirectory}/node_modules/@types`], /*recursive*/ true); - - host.renameFolder(`${currentDirectory}/node_modules2`, `${currentDirectory}/node_modules`); - host.runQueuedTimeoutCallbacks(); - checkProgramActualFiles(watch(), [file.path, libFile.path, `${currentDirectory}/node_modules/@types/qqq/index.d.ts`]); - checkOutputErrorsIncremental(host, emptyArray); - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - const projectPath = "/user/username/projects/project"; - const npmCacheFile: File = { - path: `${projectPath}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - const file1: File = { - path: `${projectPath}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectPath}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - const files = [libFile, file1, file2]; - const expectedFiles = files.map(f => f.path); - it("when watching node_modules in inferred project for failed lookup", () => { - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host, {}, /*maxNumberOfFilesToIterateForInvalidation*/ 1); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - it("when watching node_modules as part of wild card directories in config project", () => { - const config: File = { - path: `${projectPath}/tsconfig.json`, - content: "{}" - }; - const host = createWatchedSystem(files.concat(config)); - const watch = createWatchOfConfigFile(config.path, host); - checkProgramActualFiles(watch(), expectedFiles); - host.checkTimeoutQueueLength(0); - - host.ensureFileOrFolder(npmCacheFile); - host.checkTimeoutQueueLength(0); - }); - }); - }); - - describe("tsc-watch with when module emit is specified as node", () => { - it("when instead of filechanged recursive directory watcher is invoked", () => { - const configFile: File = { - path: "/a/rootFolder/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - module: "none", - allowJs: true, - outDir: "Static/scripts/" - }, - include: [ - "Scripts/**/*" - ], - }) - }; - const outputFolder = "/a/rootFolder/project/Static/scripts/"; - const file1: File = { - path: "/a/rootFolder/project/Scripts/TypeScript.ts", - content: "var z = 10;" - }; - const file2: File = { - path: "/a/rootFolder/project/Scripts/Javascript.js", - content: "var zz = 10;" - }; - const files = [configFile, file1, file2, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfConfigFile(configFile.path, host); - - checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); - file1.content = "var zz30 = 100;"; - host.reloadFS(files, { invokeDirectoryWatcherInsteadOfFileChanged: true }); - host.runQueuedTimeoutCallbacks(); - - checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); - const outputFile1 = changeExtension((outputFolder + getBaseFileName(file1.path)), ".js"); - assert.isTrue(host.fileExists(outputFile1)); - assert.equal(host.readFile(outputFile1), file1.content + host.newLine); - }); - }); - - describe("tsc-watch console clearing", () => { - const currentDirectoryLog = "Current directory: / CaseSensitiveFileNames: false\n"; - const fileWatcherAddedLog = [ - "FileWatcher:: Added:: WatchInfo: /f.ts 250 Source file\n", - "FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 Source file\n" - ]; - - const file: File = { - path: "/f.ts", - content: "" - }; - - function getProgramSynchronizingLog(options: CompilerOptions) { - return [ - "Synchronizing program\n", - "CreatingProgramWith::\n", - " roots: [\"/f.ts\"]\n", - ` options: ${JSON.stringify(options)}\n` - ]; - } - - function isConsoleClearDisabled(options: CompilerOptions) { - return options.diagnostics || options.extendedDiagnostics || options.preserveWatchOutput; - } - - function verifyCompilation(host: WatchedSystem, options: CompilerOptions, initialDisableOptions?: CompilerOptions) { - const disableConsoleClear = isConsoleClearDisabled(options); - const hasLog = options.extendedDiagnostics || options.diagnostics; - checkOutputErrorsInitial(host, emptyArray, initialDisableOptions ? isConsoleClearDisabled(initialDisableOptions) : disableConsoleClear, hasLog ? [ - currentDirectoryLog, - ...getProgramSynchronizingLog(options), - ...(options.extendedDiagnostics ? fileWatcherAddedLog : emptyArray) - ] : undefined); - host.modifyFile(file.path, "//"); - host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray, disableConsoleClear, hasLog ? [ - "FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n", - "Scheduling update\n", - "Elapsed:: 0ms FileWatcher:: Triggered with /f.ts 1:: WatchInfo: /f.ts 250 Source file\n" - ] : undefined, hasLog ? getProgramSynchronizingLog(options) : undefined); - } - - function checkConsoleClearingUsingCommandLineOptions(options: CompilerOptions = {}) { - const files = [file, libFile]; - const host = createWatchedSystem(files); - createWatchOfFilesAndCompilerOptions([file.path], host, options); - verifyCompilation(host, options); - } - - it("without --diagnostics or --extendedDiagnostics", () => { - checkConsoleClearingUsingCommandLineOptions(); - }); - it("with --diagnostics", () => { - checkConsoleClearingUsingCommandLineOptions({ - diagnostics: true, - }); - }); - it("with --extendedDiagnostics", () => { - checkConsoleClearingUsingCommandLineOptions({ - extendedDiagnostics: true, - }); - }); - it("with --preserveWatchOutput", () => { - checkConsoleClearingUsingCommandLineOptions({ - preserveWatchOutput: true, - }); - }); - - describe("when preserveWatchOutput is true in config file", () => { - const compilerOptions: CompilerOptions = { - preserveWatchOutput: true - }; - const configFile: File = { - path: "/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - const files = [file, configFile, libFile]; - it("using createWatchOfConfigFile ", () => { - const host = createWatchedSystem(files); - createWatchOfConfigFile(configFile.path, host); - // Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed - verifyCompilation(host, compilerOptions, {}); - }); - it("when createWatchProgram is invoked with configFileParseResult on WatchCompilerHostOfConfigFile", () => { - const host = createWatchedSystem(files); - const reportDiagnostic = createDiagnosticReporter(host); - const optionsToExtend: CompilerOptions = {}; - const configParseResult = parseConfigFileWithSystem(configFile.path, optionsToExtend, host, reportDiagnostic)!; - const watchCompilerHost = createWatchCompilerHostOfConfigFile(configParseResult.options.configFilePath!, optionsToExtend, host, /*createProgram*/ undefined, reportDiagnostic, createWatchStatusReporter(host)); - watchCompilerHost.configFileParsingResult = configParseResult; - createWatchProgram(watchCompilerHost); - verifyCompilation(host, compilerOptions); - }); - }); - }); - - describe("tsc-watch with different polling/non polling options", () => { - it("watchFile using dynamic priority polling", () => { - const projectFolder = "/a/username/project"; - const file1: File = { - path: `${projectFolder}/typescript.ts`, - content: "var z = 10;" - }; - const files = [file1, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - - const initialProgram = watch(); - verifyProgram(); - - const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; - for (let index = 0; index < mediumPollingIntervalThreshold; index++) { - // Transition libFile and file1 to low priority queue - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), initialProgram); - } - - // Make a change to file - file1.content = "var zz30 = 100;"; - host.reloadFS(files); - - // This should detect change in the file - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), initialProgram); - - // Callbacks: medium priority + high priority queue and scheduled program update - host.checkTimeoutQueueLengthAndRun(3); - // During this timeout the file would be detected as unchanged - let fileUnchangeDetected = 1; - const newProgram = watch(); - assert.notStrictEqual(newProgram, initialProgram); - - verifyProgram(); - const outputFile1 = changeExtension(file1.path, ".js"); - assert.isTrue(host.fileExists(outputFile1)); - assert.equal(host.readFile(outputFile1), file1.content + host.newLine); - - const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; - for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { - // For high + Medium/low polling interval - host.checkTimeoutQueueLengthAndRun(2); - assert.deepEqual(watch(), newProgram); - } - - // Everything goes in high polling interval queue - host.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(watch(), newProgram); - - function verifyProgram() { - checkProgramActualFiles(watch(), files.map(f => f.path)); - checkWatchedFiles(host, []); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, [], /*recursive*/ true); - } - }); - - describe("tsc-watch when watchDirectories implementation", () => { - function verifyRenamingFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const file: File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" - }; - const programFiles = [file, libFile]; - const files = [file, configFile, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - const host = createWatchedSystem(files, { environmentVariables }); - const watch = createWatchOfConfigFile(configFile.path, host); - const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; - // Watching files config file, file, lib file - const expectedWatchedFiles = files.map(f => f.path); - const expectedWatchedDirectories = tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; - if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { - expectedWatchedFiles.push(...projectFolders); - } - - verifyProgram(checkOutputErrorsInitial); - - // Rename the file: - file.path = file.path.replace("file1.ts", "file2.ts"); - expectedWatchedFiles[0] = file.path; - host.reloadFS(files); - if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { - // With dynamic polling the fs change would be detected only by running timeouts - host.runQueuedTimeoutCallbacks(); - } - // Delayed update program - host.runQueuedTimeoutCallbacks(); - verifyProgram(checkOutputErrorsIncremental); - - function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { - checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray); - - const outputFile = changeExtension(file.path, ".js"); - assert(host.fileExists(outputFile)); - assert.equal(host.readFile(outputFile), file.content); - - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - - // Watching config file, file, lib file and directories - checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories, 1, /*recursive*/ false); - } - } - - it("uses watchFile when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.WatchFile); - }); - - it("uses non recursive watchDirectory when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.NonRecursiveWatchDirectory); - }); - - it("uses non recursive dynamic polling when renaming file in subfolder", () => { - verifyRenamingFileInSubFolder(Tsc_WatchDirectory.DynamicPolling); - }); - - it("when there are symlinks to folders in recursive folders", () => { - const cwd = "/home/user/projects/myproject"; - const file1: File = { - path: `${cwd}/src/file.ts`, - content: `import * as a from "a"` - }; - const tsconfig: File = { - path: `${cwd}/tsconfig.json`, - content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` - }; - const realA: File = { - path: `${cwd}/node_modules/reala/index.d.ts`, - content: `export {}` - }; - const realB: File = { - path: `${cwd}/node_modules/realb/index.d.ts`, - content: `export {}` - }; - const symLinkA: SymLink = { - path: `${cwd}/node_modules/a`, - symLink: `${cwd}/node_modules/reala` - }; - const symLinkB: SymLink = { - path: `${cwd}/node_modules/b`, - symLink: `${cwd}/node_modules/realb` - }; - const symLinkBInA: SymLink = { - path: `${cwd}/node_modules/reala/node_modules/b`, - symLink: `${cwd}/node_modules/b` - }; - const symLinkAInB: SymLink = { - path: `${cwd}/node_modules/realb/node_modules/a`, - symLink: `${cwd}/node_modules/a` - }; - const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); - const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); - checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, - `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); - }); - }); - }); - - describe("tsc-watch with modules linked to sibling folder", () => { - const projectRoot = "/user/username/projects/project"; - const mainPackageRoot = `${projectRoot}/main`; - const linkedPackageRoot = `${projectRoot}/linked-package`; - const mainFile: File = { - path: `${mainPackageRoot}/index.ts`, - content: "import { Foo } from '@scoped/linked-package'" - }; - const config: File = { - path: `${mainPackageRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, - files: ["index.ts"] - }) - }; - const linkedPackageInMain: SymLink = { - path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, - symLink: `${linkedPackageRoot}` - }; - const linkedPackageJson: File = { - path: `${linkedPackageRoot}/package.json`, - content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) - }; - const linkedPackageIndex: File = { - path: `${linkedPackageRoot}/dist/index.d.ts`, - content: "export * from './other';" - }; - const linkedPackageOther: File = { - path: `${linkedPackageRoot}/dist/other.d.ts`, - content: 'export declare const Foo = "BAR";' - }; - - it("verify watched directories", () => { - const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; - const host = createWatchedSystem(files, { currentDirectory: mainPackageRoot }); - createWatchOfConfigFile("tsconfig.json", host); - checkWatchedFilesDetailed(host, [libFile.path, mainFile.path, config.path, linkedPackageIndex.path, linkedPackageOther.path], 1); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, [`${mainPackageRoot}/@scoped`, `${mainPackageRoot}/node_modules`, linkedPackageRoot, `${mainPackageRoot}/node_modules/@types`, `${projectRoot}/node_modules/@types`], 1, /*recursive*/ true); - }); - }); - - describe("tsc-watch with custom module resolution", () => { - const projectRoot = "/user/username/projects/project"; - const configFileJson: any = { - compilerOptions: { module: "commonjs", resolveJsonModule: true }, - files: ["index.ts"] - }; - const mainFile: File = { - path: `${projectRoot}/index.ts`, - content: "import settings from './settings.json';" - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify(configFileJson) - }; - const settingsJson: File = { - path: `${projectRoot}/settings.json`, - content: JSON.stringify({ content: "Print this" }) - }; - - it("verify that module resolution with json extension works when returned without extension", () => { - const files = [libFile, mainFile, config, settingsJson]; - const host = createWatchedSystem(files, { currentDirectory: projectRoot }); - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, host); - const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); - compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { - const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); - const resolvedModule = result.resolvedModule!; - return { - resolvedFileName: resolvedModule.resolvedFileName, - isExternalLibraryImport: resolvedModule.isExternalLibraryImport, - originalFileName: resolvedModule.originalPath, - }; - }); - const watch = createWatchProgram(compilerHost); - const program = watch.getCurrentProgram().getProgram(); - checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); - }); - }); -} diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts new file mode 100644 index 00000000000..26b7857347a --- /dev/null +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -0,0 +1,704 @@ +namespace ts.projectSystem { + function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { + return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); + } + + describe("unittests:: tsserver:: CachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { + enum CalledMapsWithSingleArg { + fileExists = "fileExists", + directoryExists = "directoryExists", + getDirectories = "getDirectories", + readFile = "readFile" + } + enum CalledMapsWithFiveArgs { + readDirectory = "readDirectory" + } + type CalledMaps = CalledMapsWithSingleArg | CalledMapsWithFiveArgs; + function createCallsTrackingHost(host: TestServerHost) { + const calledMaps: Record> & Record, ReadonlyArray, ReadonlyArray, number]>> = { + fileExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.fileExists), + directoryExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.directoryExists), + getDirectories: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.getDirectories), + readFile: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.readFile), + readDirectory: setCallsTrackingWithFiveArgFn(CalledMapsWithFiveArgs.readDirectory) + }; + + return { + verifyNoCall, + verifyCalledOnEachEntryNTimes, + verifyCalledOnEachEntry, + verifyNoHostCalls, + verifyNoHostCallsExceptFileExistsOnce, + verifyCalledOn, + clear + }; + + function setCallsTrackingWithSingleArgFn(prop: CalledMapsWithSingleArg) { + const calledMap = createMultiMap(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string) => { + calledMap.add(f, /*value*/ true); + return cb(f); + }; + return calledMap; + } + + function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { + const calledMap = createMultiMap<[U, V, W, X]>(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { + calledMap.add(f, [arg1!, arg2!, arg3!, arg4!]); // TODO: GH#18217 + return cb(f, arg1, arg2, arg3, arg4); + }; + return calledMap; + } + + function verifyCalledOn(callback: CalledMaps, name: string) { + const calledMap = calledMaps[callback]; + const result = calledMap.get(name); + assert.isTrue(result && !!result.length, `${callback} should be called with name: ${name}: ${arrayFrom(calledMap.keys())}`); + } + + function verifyNoCall(callback: CalledMaps) { + const calledMap = calledMaps[callback]; + assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${arrayFrom(calledMap.keys())}`); + } + + function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { + TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); + } + + function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: ReadonlyArray, nTimes: number) { + TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys, nTimes); + } + + function verifyNoHostCalls() { + iterateOnCalledMaps(key => verifyNoCall(key)); + } + + function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: ReadonlyArray) { + verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, expectedKeys, 1); + verifyNoCall(CalledMapsWithSingleArg.directoryExists); + verifyNoCall(CalledMapsWithSingleArg.getDirectories); + verifyNoCall(CalledMapsWithSingleArg.readFile); + verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + } + + function clear() { + iterateOnCalledMaps(key => calledMaps[key].clear()); + } + + function iterateOnCalledMaps(cb: (key: CalledMaps) => void) { + for (const key in CalledMapsWithSingleArg) { + cb(key as CalledMapsWithSingleArg); + } + for (const key in CalledMapsWithFiveArgs) { + cb(key as CalledMapsWithFiveArgs); + } + } + } + + it("works using legacy resolution logic", () => { + let rootContent = `import {x} from "f1"`; + const root: File = { + path: "/c/d/f0.ts", + content: rootContent + }; + + const imported: File = { + path: "/c/f1.ts", + content: `foo()` + }; + + const host = createServerHost([root, imported]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + + // ensure that imported file was found + verifyImportedDiagnostics(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // trigger synchronization to make sure that import will be fetched from the cache + // ensure file has correct number of errors after edit + editContent(`import {x} from "f1"; + var x: string = 1;`); + verifyImportedDiagnostics(); + callsTrackingHost.verifyNoHostCalls(); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + editContent(`import {x} from "f2"`); + try { + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + verifyImportedDiagnostics(); + assert.isTrue(false, `should not find file '${imported.path}'`); + } + catch (e) { + assert.isTrue(e.message.indexOf(`Could not find file: '${imported.path}'.`) === 0); + } + const f2Lookups = getLocationsForModuleLookup("f2"); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f2Lookups, 1); + const f2DirLookups = getLocationsForDirectoryLookup(); + callsTrackingHost.verifyCalledOnEachEntry(CalledMapsWithSingleArg.directoryExists, f2DirLookups); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + + editContent(`import {x} from "f1"`); + verifyImportedDiagnostics(); + const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); + f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; + const f1DirLookups = ["/c/d", "/c", ...mapCombinedPathsInAncestor(getDirectoryPath(root.path), nodeModulesAtTypes, returnTrue)]; + vertifyF1Lookups(); + + // setting compiler options discards module resolution cache + callsTrackingHost.clear(); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true, target: ScriptTarget.ES5 }); + verifyImportedDiagnostics(); + vertifyF1Lookups(); + + function vertifyF1Lookups() { + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f1Lookups, 1); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, f1DirLookups, 1); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + } + + function editContent(newContent: string) { + callsTrackingHost.clear(); + rootScriptInfo.editContent(0, rootContent.length, newContent); + rootContent = newContent; + } + + function verifyImportedDiagnostics() { + const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_name_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); + } + + function getLocationsForModuleLookup(module: string) { + const locations: string[] = []; + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push( + combinePaths(ancestor, `${module}.ts`), + combinePaths(ancestor, `${module}.tsx`), + combinePaths(ancestor, `${module}.d.ts`) + ); + }); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push( + combinePaths(ancestor, `${module}.js`), + combinePaths(ancestor, `${module}.jsx`) + ); + }); + return locations; + } + + function getLocationsForDirectoryLookup() { + const result = createMap(); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + // To resolve modules + result.set(ancestor, 2); + // for type roots + result.set(combinePaths(ancestor, nodeModules), 1); + result.set(combinePaths(ancestor, nodeModulesAtTypes), 1); + }); + return result; + } + }); + + it("loads missing files from disk", () => { + const root: File = { + path: "/c/foo.ts", + content: `import {y} from "bar"` + }; + + const imported: File = { + path: "/c/bar.d.ts", + content: `export var y = 1` + }; + + const host = createServerHost([root]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); + const callsTrackingHost = createCallsTrackingHost(host); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + + let diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_module_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'."); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + + + callsTrackingHost.clear(); + host.reloadFS([root, imported]); + host.runQueuedTimeoutCallbacks(); + diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 0); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + }); + + it("when calling goto definition of module", () => { + const clientFile: File = { + path: "/a/b/controllers/vessels/client.ts", + content: ` + import { Vessel } from '~/models/vessel'; + const v = new Vessel(); + ` + }; + const anotherModuleFile: File = { + path: "/a/b/utils/db.ts", + content: "export class Bookshelf { }" + }; + const moduleFile: File = { + path: "/a/b/models/vessel.ts", + content: ` + import { Bookshelf } from '~/utils/db'; + export class Vessel extends Bookshelf {} + ` + }; + const tsconfigFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + target: "es6", + module: "es6", + baseUrl: "./", // all paths are relative to the baseUrl + paths: { + "~/*": ["*"] // resolve any `~/foo/bar` to `/foo/bar` + } + }, + exclude: [ + "api", + "build", + "node_modules", + "public", + "seeds", + "sql_updates", + "tests.build" + ] + }) + }; + const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; + const host = createServerHost(projectFiles); + const session = createSession(host); + const projectService = session.getProjectService(); + const { configFileName } = projectService.openClientFile(clientFile.path); + + assert.isDefined(configFileName, `should find config`); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigFile.path)!; + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Get definitions shouldnt make host requests + const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { + file: clientFile.path, + position: clientFile.content.indexOf("/vessel") + 1, + line: undefined!, // TODO: GH#18217 + offset: undefined! // TODO: GH#18217 + }); + const response = session.executeCommand(getDefinitionRequest).response as server.protocol.FileSpan[]; + assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); + callsTrackingHost.verifyNoHostCalls(); + + // Open the file should call only file exists on module directory and use cached value for parental directory + const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); + assert.equal(config2, configFileName); + callsTrackingHost.verifyNoHostCallsExceptFileExistsOnce(["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); + }); + + describe("WatchDirectories for config file with", () => { + function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const frontendDir = "/Users/someuser/work/applications/frontend"; + const toCanonical: (s: string) => Path = useCaseSensitiveFileNames ? s => s as Path : s => s.toLowerCase() as Path; + const canonicalFrontendDir = toCanonical(frontendDir); + const file1: File = { + path: `${frontendDir}/src/app/utils/Analytic.ts`, + content: "export class SomeClass { };" + }; + const file2: File = { + path: `${frontendDir}/src/app/redux/configureStore.ts`, + content: "export class configureStore { }" + }; + const file3: File = { + path: `${frontendDir}/src/app/utils/Cookie.ts`, + content: "export class Cookie { }" + }; + const es2016LibFile: File = { + path: "/a/lib/lib.es2016.full.d.ts", + content: libFile.content + }; + const typeRoots = ["types", "node_modules/@types"]; + const types = ["node", "jest"]; + const tsconfigFile: File = { + path: `${frontendDir}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + strict: true, + strictNullChecks: true, + target: "es2016", + module: "commonjs", + moduleResolution: "node", + sourceMap: true, + noEmitOnError: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + types, + noUnusedLocals: true, + outDir: "./compiled", + typeRoots, + baseUrl: ".", + paths: { + "*": [ + "types/*" + ] + } + }, + include: [ + "src/**/*" + ], + exclude: [ + "node_modules", + "compiled" + ] + }) + }; + const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; + const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host); + const canonicalConfigPath = toCanonical(tsconfigFile.path); + const { configFileName } = projectService.openClientFile(file1.path); + assert.equal(configFileName, tsconfigFile.path as server.NormalizedPath, `should find config`); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217) + checkNumberOfConfiguredProjects(projectService, 1); + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); + + const project = projectService.configuredProjects.get(canonicalConfigPath)!; + verifyProjectAndWatchedDirectories(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Create file cookie.ts + projectFiles.push(file3); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + + const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); + const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, [canonicalFile3Path], numberOfTimesWatchInvoked); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, [canonicalFile3Path], numberOfTimesWatchInvoked); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.readFile, [file3.path], 1); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + + callsTrackingHost.clear(); + + const { configFileName: configFile2 } = projectService.openClientFile(file3.path); + assert.equal(configFile2, configFileName); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.verifyNoHostCalls(); + + function getFilePathIfNotOpen(f: File) { + const path = toCanonical(f.path); + const info = projectService.getScriptInfoForPath(toCanonical(f.path)); + return info && info.isScriptOpen() ? undefined : path; + } + + function verifyProjectAndWatchedDirectories() { + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); + checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + } + + it("case insensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + + it("case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + }); + + describe("Subfolder invalidations correctly include parent folder failed lookup locations", () => { + function runFailedLookupTest(resolution: "Node" | "Classic") { + const projectLocation = "/proj"; + const file1: File = { + path: `${projectLocation}/foo/boo/app.ts`, + content: `import * as debug from "debug"` + }; + const file2: File = { + path: `${projectLocation}/foo/boo/moo/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + files: ["foo/boo/app.ts", "foo/boo/moo/app.ts"], + moduleResolution: resolution + }) + }; + + const files = [file1, file2, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + + const debugTypesFile: File = { + path: `${projectLocation}/node_modules/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), []); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), []); + } + + it("Includes the parent folder FLLs in node module resolution mode", () => { + runFailedLookupTest("Node"); + }); + it("Includes the parent folder FLLs in classic module resolution mode", () => { + runFailedLookupTest("Classic"); + }); + }); + + describe("Verify npm install in directory with tsconfig file works when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const root = "/user/username/rootfolder/otherfolder"; + const getRootedFileOrFolder = (fileOrFolder: File) => { + fileOrFolder.path = root + fileOrFolder.path; + return fileOrFolder; + }; + const app: File = getRootedFileOrFolder({ + path: "/a/b/app.ts", + content: "import _ from 'lodash';" + }); + const tsconfigJson: File = getRootedFileOrFolder({ + path: "/a/b/tsconfig.json", + content: '{ "compilerOptions": { } }' + }); + const packageJson: File = getRootedFileOrFolder({ + path: "/a/b/package.json", + content: ` +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "lodash", + "rxjs" + }, + "devDependencies": { + "@types/lodash", + "typescript" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} +` + }); + const appFolder = getDirectoryPath(app.path); + const projectFiles = [app, libFile, tsconfigJson]; + const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); + const otherFiles = [packageJson]; + const host = createServerHost(projectFiles.concat(otherFiles)); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(app.path); + assert.equal(configFileName, tsconfigJson.path as server.NormalizedPath, `should find config`); // TODO: GH#18217 + const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); + verifyProject(); + + let npmInstallComplete = false; + + // Simulate npm install + const filesAndFoldersToAdd: File[] = [ + { path: "/a/b/node_modules" }, + { path: "/a/b/node_modules/.staging/@types" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/package.json", content: "{\n \"name\": \"symbol-observable\",\n \"version\": \"1.0.4\",\n \"description\": \"Symbol.observable ponyfill\",\n \"license\": \"MIT\",\n \"repository\": \"blesh/symbol-observable\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n },\n \"scripts\": {\n \"test\": \"npm run build && mocha && tsc ./ts-test/test.ts && node ./ts-test/test.js && check-es3-syntax -p lib/ --kill\",\n \"build\": \"babel es --out-dir lib\",\n \"prepublish\": \"npm test\"\n },\n \"files\": [\n \"" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa/package.json", content: "{\n \"name\": \"lodash\",\n \"version\": \"4.17.4\",\n \"description\": \"Lodash modular utilities.\",\n \"keywords\": \"modules, stdlib, util\",\n \"homepage\": \"https://lodash.com/\",\n \"repository\": \"lodash/lodash\",\n \"icon\": \"https://lodash.com/icon.svg\",\n \"license\": \"MIT\",\n \"main\": \"lodash.js\",\n \"author\": \"John-David Dalton (http://allyoucanleet.com/)\",\n \"contributors\": [\n \"John-David Dalton (http://allyoucanleet.com/)\",\n \"Mathias Bynens \",\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"license\": \"Apache-2.0\",\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"tslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"typings\": \"Rx.d.ts\",\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n }\n}" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json", content: "{\n \"name\": \"typescript\",\n \"author\": \"Microsoft Corp.\",\n \"homepage\": \"http://typescriptlang.org/\",\n \"version\": \"2.4.2\",\n \"license\": \"Apache-2.0\",\n \"description\": \"TypeScript is a language for application scale JavaScript development\",\n \"keywords\": [\n \"TypeScript\",\n \"Microsoft\",\n \"compiler\",\n \"language\",\n \"javascript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/Microsoft/TypeScript/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Microsoft/TypeScript.git\"\n },\n \"main\": \"./lib/typescript.js\",\n \"typings\": \"./lib/typescript.d.ts\",\n \"bin\": {\n \"tsc\": \"./bin/tsc\",\n \"tsserver\": \"./bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n },\n \"devDependencies\": {\n \"@types/browserify\": \"latest\",\n \"@types/chai\": \"latest\",\n \"@types/convert-source-map\": \"latest\",\n \"@types/del\": \"latest\",\n \"@types/glob\": \"latest\",\n \"@types/gulp\": \"latest\",\n \"@types/gulp-concat\": \"latest\",\n \"@types/gulp-help\": \"latest\",\n \"@types/gulp-newer\": \"latest\",\n \"@types/gulp-sourcemaps\": \"latest\",\n \"@types/merge2\": \"latest\",\n \"@types/minimatch\": \"latest\",\n \"@types/minimist\": \"latest\",\n \"@types/mkdirp\": \"latest\",\n \"@types/mocha\": \"latest\",\n \"@types/node\": \"latest\",\n \"@types/q\": \"latest\",\n \"@types/run-sequence\": \"latest\",\n \"@types/through2\": \"latest\",\n \"browserify\": \"latest\",\n \"chai\": \"latest\",\n \"convert-source-map\": \"latest\",\n \"del\": \"latest\",\n \"gulp\": \"latest\",\n \"gulp-clone\": \"latest\",\n \"gulp-concat\": \"latest\",\n \"gulp-help\": \"latest\",\n \"gulp-insert\": \"latest\",\n \"gulp-newer\": \"latest\",\n \"gulp-sourcemaps\": \"latest\",\n \"gulp-typescript\": \"latest\",\n \"into-stream\": \"latest\",\n \"istanbul\": \"latest\",\n \"jake\": \"latest\",\n \"merge2\": \"latest\",\n \"minimist\": \"latest\",\n \"mkdirp\": \"latest\",\n \"mocha\": \"latest\",\n \"mocha-fivemat-progress-reporter\": \"latest\",\n \"q\": \"latest\",\n \"run-sequence\": \"latest\",\n \"sorcery\": \"latest\",\n \"through2\": \"latest\",\n \"travis-fold\": \"latest\",\n \"ts-node\": \"latest\",\n \"tslint\": \"latest\",\n \"typescript\": \"^2.4\"\n },\n \"scripts\": {\n \"pretest\": \"jake tests\",\n \"test\": \"jake runtests-parallel\",\n \"build\": \"npm run build:compiler && npm run build:tests\",\n \"build:compiler\": \"jake local\",\n \"build:tests\": \"jake tests\",\n \"start\": \"node lib/tsc\",\n \"clean\": \"jake clean\",\n \"gulp\": \"gulp\",\n \"jake\": \"jake\",\n \"lint\": \"jake lint\",\n \"setup-hooks\": \"node scripts/link-hooks.js\"\n },\n \"browser\": {\n \"buffer\": false,\n \"fs\": false,\n \"os\": false,\n \"path\": false\n }\n}" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.js", content: "module.exports = require('./lib/index');\n" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.d.ts", content: "declare const observableSymbol: symbol;\nexport default observableSymbol;\n" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib/index.js", content: "'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _ponyfill = require('./ponyfill');\n\nvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\nvar root; /* global window */\n\n\nif (typeof self !== 'undefined') {\n root = self;\n} else if (typeof window !== 'undefined') {\n root = window;\n} else if (typeof global !== 'undefined') {\n root = global;\n} else if (typeof module !== 'undefined') {\n root = module;\n} else {\n root = Function('return this')();\n}\n\nvar result = (0, _ponyfill2['default'])(root);\nexports['default'] = result;" }, + ].map(getRootedFileOrFolder); + verifyAfterPartialOrCompleteNpmInstall(2); + + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/lib" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/add/operator" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/package.json", content: "{\n \"name\": \"@types/lodash\",\n \"version\": \"4.14.74\",\n \"description\": \"TypeScript definitions for Lo-Dash\",\n \"license\": \"MIT\",\n \"contributors\": [\n {\n \"name\": \"Brian Zengel\",\n \"url\": \"https://github.com/bczengel\"\n },\n {\n \"name\": \"Ilya Mochalov\",\n \"url\": \"https://github.com/chrootsu\"\n },\n {\n \"name\": \"Stepan Mikhaylyuk\",\n \"url\": \"https://github.com/stepancar\"\n },\n {\n \"name\": \"Eric L Anderson\",\n \"url\": \"https://github.com/ericanderson\"\n },\n {\n \"name\": \"AJ Richardson\",\n \"url\": \"https://github.com/aj-r\"\n },\n {\n \"name\": \"Junyoung Clare Jang\",\n \"url\": \"https://github.com/ailrun\"\n }\n ],\n \"main\": \"\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\"\n },\n \"scripts\": {},\n \"dependencies\": {},\n \"typesPublisherContentHash\": \"12af578ffaf8d86d2df37e591857906a86b983fa9258414326544a0fe6af0de8\",\n \"typeScriptVersion\": \"2.2\"\n}" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa/index.js", content: "module.exports = require('./lodash');" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" } + ].map(getRootedFileOrFolder)); + // Since we added/removed in .staging no timeout + verifyAfterPartialOrCompleteNpmInstall(0); + + // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" + filesAndFoldersToAdd.length--; + verifyAfterPartialOrCompleteNpmInstall(0); + + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", content: "\n// Stub for lodash\nexport = _;\nexport as namespace _;\ndeclare var _: _.LoDashStatic;\ndeclare namespace _ {\n interface LoDashStatic {\n someProp: string;\n }\n class SomeClass {\n someMethod(): void;\n }\n}" } + ].map(getRootedFileOrFolder)); + verifyAfterPartialOrCompleteNpmInstall(0); + + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/scheduler" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/util" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/symbol" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/testing" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041", content: "{\n \"_args\": [\n [\n {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\"\n ]\n ],\n \"_from\": \"rxjs@>=5.4.2 <6.0.0\",\n \"_id\": \"rxjs@5.4.3\",\n \"_inCache\": true,\n \"_location\": \"/rxjs\",\n \"_nodeVersion\": \"7.7.2\",\n \"_npmOperationalInternal\": {\n \"host\": \"s3://npm-registry-packages\",\n \"tmp\": \"tmp/rxjs-5.4.3.tgz_1502407898166_0.6800217325799167\"\n },\n \"_npmUser\": {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"_npmVersion\": \"5.3.0\",\n \"_phantomChildren\": {},\n \"_requested\": {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"_requiredBy\": [\n \"/\"\n ],\n \"_resolved\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\",\n \"_shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"_shrinkwrap\": null,\n \"_spec\": \"rxjs@^5.4.2\",\n \"_where\": \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"config\": {\n \"commitizen\": {\n \"path\": \"cz-conventional-changelog\"\n }\n },\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n },\n \"description\": \"Reactive Extensions for modern JavaScript\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"tslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"directories\": {},\n \"dist\": {\n \"integrity\": \"sha512-fSNi+y+P9ss+EZuV0GcIIqPUK07DEaMRUtLJvdcvMyFjc9dizuDjere+A4V7JrLGnm9iCc+nagV/4QdMTkqC4A==\",\n \"shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"tarball\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"keywords\": [\n \"Rx\",\n \"RxJS\",\n \"ReactiveX\",\n \"ReactiveExtensions\",\n \"Streams\",\n \"Observables\",\n \"Observable\",\n \"Stream\",\n \"ES6\",\n \"ES2015\"\n ],\n \"license\": \"Apache-2.0\",\n \"lint-staged\": {\n \"*.@(js)\": [\n \"eslint --fix\",\n \"git add\"\n ],\n \"*.@(ts)\": [\n \"tslint --fix\",\n \"git add\"\n ]\n },\n \"main\": \"Rx.js\",\n \"maintainers\": [\n {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n }\n ],\n \"name\": \"rxjs\",\n \"optionalDependencies\": {},\n \"readme\": \"ERROR: No README data found!\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+ssh://git@github.com/ReactiveX/RxJS.git\"\n },\n \"scripts-info\": {\n \"info\": \"List available script\",\n \"build_all\": \"Build all packages (ES6, CJS, UMD) and generate packages\",\n \"build_cjs\": \"Build CJS package with clean up existing build, copy source into dist\",\n \"build_es6\": \"Build ES6 package with clean up existing build, copy source into dist\",\n \"build_closure_core\": \"Minify Global core build using closure compiler\",\n \"build_global\": \"Build Global package, then minify build\",\n \"build_perf\": \"Build CJS & Global build, run macro performance test\",\n \"build_test\": \"Build CJS package & test spec, execute mocha test runner\",\n \"build_cover\": \"Run lint to current code, build CJS & test spec, execute test coverage\",\n \"build_docs\": \"Build ES6 & global package, create documentation using it\",\n \"build_spec\": \"Build test specs\",\n \"check_circular_dependencies\": \"Check codebase has circular dependencies\",\n \"clean_spec\": \"Clean up existing test spec build output\",\n \"clean_dist_cjs\": \"Clean up existing CJS package output\",\n \"clean_dist_es6\": \"Clean up existing ES6 package output\",\n \"clean_dist_global\": \"Clean up existing Global package output\",\n \"commit\": \"Run git commit wizard\",\n \"compile_dist_cjs\": \"Compile codebase into CJS module\",\n \"compile_module_es6\": \"Compile codebase into ES6\",\n \"cover\": \"Execute test coverage\",\n \"lint_perf\": \"Run lint against performance test suite\",\n \"lint_spec\": \"Run lint against test spec\",\n \"lint_src\": \"Run lint against source\",\n \"lint\": \"Run lint against everything\",\n \"perf\": \"Run macro performance benchmark\",\n \"perf_micro\": \"Run micro performance benchmark\",\n \"test_mocha\": \"Execute mocha test runner against existing test spec build\",\n \"test_browser\": \"Execute mocha test runner on browser against existing test spec build\",\n \"test\": \"Clean up existing test spec build, build test spec and execute mocha test runner\",\n \"tests2png\": \"Generate marble diagram image from test spec\",\n \"watch\": \"Watch codebase, trigger compile when source code changes\"\n },\n \"typings\": \"Rx.d.ts\",\n \"version\": \"5.4.3\"\n}\n" } + ].map(getRootedFileOrFolder)); + verifyAfterPartialOrCompleteNpmInstall(0); + + // remove /a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041 + filesAndFoldersToAdd.length--; + // and add few more folders/files + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/symbol-observable" }, + { path: "/a/b/node_modules/@types" }, + { path: "/a/b/node_modules/@types/lodash" }, + { path: "/a/b/node_modules/lodash" }, + { path: "/a/b/node_modules/rxjs" }, + { path: "/a/b/node_modules/typescript" }, + { path: "/a/b/node_modules/.bin" } + ].map(getRootedFileOrFolder)); + // From the type root update + verifyAfterPartialOrCompleteNpmInstall(2); + + forEach(filesAndFoldersToAdd, f => { + f.path = f.path + .replace("/a/b/node_modules/.staging", "/a/b/node_modules") + .replace(/[\-\.][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w]/g, ""); + }); + + const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; + projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)!); + // we would now not have failed lookup in the parent of appFolder since lodash is available + recursiveWatchedDirectories.length = 2; + // npm installation complete, timeout after reload fs + npmInstallComplete = true; + verifyAfterPartialOrCompleteNpmInstall(2); + + function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { + host.reloadFS(projectFiles.concat(otherFiles, filesAndFoldersToAdd)); + if (npmInstallComplete || timeoutDuringPartialInstallation) { + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); + } + else { + host.checkTimeoutQueueLength(2); + } + verifyProject(); + } + + function verifyProject() { + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigJson.path)!; + const projectFilePaths = map(projectFiles, f => f.path); + checkProjectActualFiles(project, projectFilePaths); + + const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); + checkWatchedFiles(host, filesWatched); + checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + } + + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); + }); + + it("timeout occurs after installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); + }); + }); + + it("when node_modules dont receive event for the @types file addition", () => { + const projectLocation = "/user/username/folder/myproject"; + const app: File = { + path: `${projectLocation}/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: "" + }; + + const files = [app, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(app.path); + + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + + const debugTypesFile: File = { + path: `${projectLocation}/node_modules/@types/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + // Do not invoke recursive directory watcher for anything other than node_module/@types + const invoker = host.invokeWatchedDirectoriesRecursiveCallback; + host.invokeWatchedDirectoriesRecursiveCallback = (fullPath, relativePath) => { + if (fullPath.endsWith("@types")) { + invoker.call(host, fullPath, relativePath); + } + }; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/cancellationToken.ts b/src/testRunner/unittests/tsserver/cancellationToken.ts new file mode 100644 index 00000000000..71fb4bb4b22 --- /dev/null +++ b/src/testRunner/unittests/tsserver/cancellationToken.ts @@ -0,0 +1,271 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: cancellationToken", () => { + // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test + let oldPrepare: AnyFunction; + before(() => { + oldPrepare = (Error as any).prepareStackTrace; + delete (Error as any).prepareStackTrace; + }); + + after(() => { + (Error as any).prepareStackTrace = oldPrepare; + }); + + it("is attached to request", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let xyz = 1;" + }; + const host = createServerHost([f1]); + let expectedRequestId: number; + const cancellationToken: server.ServerCancellationToken = { + isCancellationRequested: () => false, + setRequest: requestId => { + if (expectedRequestId === undefined) { + assert.isTrue(false, "unexpected call"); + } + assert.equal(requestId, expectedRequestId); + }, + resetRequest: noop + }; + + const session = createSession(host, { cancellationToken }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq({ + command: "occurrences", + arguments: { file: f1.path, line: 1, offset: 6 } + }); + + expectedRequestId = 2; + host.runQueuedImmediateCallbacks(); + expectedRequestId = 2; + host.runQueuedImmediateCallbacks(); + }); + + it("Geterr is cancellable", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {} + }) + }; + + const cancellationToken = new TestServerCancellationToken(); + const host = createServerHost([f1, config]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: noop, + cancellationToken + }); + { + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + }); + // send geterr for missing file + session.executeCommandSeq({ + command: "geterr", + arguments: { files: ["/a/missing"] } + }); + // no files - expect 'completed' event + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(session.getSeq(), 0); + } + { + const getErrId = session.getNextSeq(); + // send geterr for a valid file + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + + // run new request + session.executeCommandSeq({ + command: "projectInfo", + arguments: { file: f1.path } + }); + session.clearMessages(); + + // cancel previously issued Geterr + cancellationToken.setRequestToCancel(getErrId); + host.runQueuedTimeoutCallbacks(); + + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); + + cancellationToken.resetToken(); + } + { + const getErrId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = getMessage(0); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + + cancellationToken.setRequestToCancel(getErrId); + host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); + + cancellationToken.resetToken(); + } + { + const getErrId = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = getMessage(0); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + + // the semanticDiag message + host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 1); + const e2 = getMessage(0); + assert.equal(e2.event, "semanticDiag"); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + assert.equal(host.getOutput().length, 2); + const e3 = getMessage(0); + assert.equal(e3.event, "suggestionDiag"); + verifyRequestCompleted(getErrId, 1); + + cancellationToken.resetToken(); + } + { + const getErr1 = session.getNextSeq(); + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = getMessage(0); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + + session.executeCommandSeq({ + command: "geterr", + arguments: { files: [f1.path] } + }); + // make sure that getErr1 is completed + verifyRequestCompleted(getErr1, 0); + } + + function verifyRequestCompleted(expectedSeq: number, n: number) { + const event = getMessage(n); + assert.equal(event.event, "requestCompleted"); + assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); + session.clearMessages(); + } + + function getMessage(n: number) { + return JSON.parse(server.extractMessage(host.getOutput()[n])); + } + }); + + it("Lower priority tasks are cancellable", () => { + const f1 = { + path: "/a/app.ts", + content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";` + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {} + }) + }; + const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3); + const host = createServerHost([f1, config]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: noop, + cancellationToken, + throttleWaitMilliseconds: 0 + }); + { + session.executeCommandSeq({ + command: "open", + arguments: { file: f1.path } + }); + + // send navbar request (normal priority) + session.executeCommandSeq({ + command: "navbar", + arguments: { file: f1.path } + }); + + // ensure the nav bar request can be canceled + verifyExecuteCommandSeqIsCancellable({ + command: "navbar", + arguments: { file: f1.path } + }); + + // send outlining spans request (normal priority) + session.executeCommandSeq({ + command: "outliningSpans", + arguments: { file: f1.path } + }); + + // ensure the outlining spans request can be canceled + verifyExecuteCommandSeqIsCancellable({ + command: "outliningSpans", + arguments: { file: f1.path } + }); + } + + function verifyExecuteCommandSeqIsCancellable(request: Partial) { + // Set the next request to be cancellable + // The cancellation token will cancel the request the third time + // isCancellationRequested() is called. + cancellationToken.setRequestToCancel(session.getNextSeq()); + let operationCanceledExceptionThrown = false; + + try { + session.executeCommandSeq(request); + } + catch (e) { + assert(e instanceof OperationCanceledException); + operationCanceledExceptionThrown = true; + } + assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request)); + } + }); + }); +} diff --git a/src/testRunner/unittests/compileOnSave.ts b/src/testRunner/unittests/tsserver/compileOnSave.ts similarity index 81% rename from src/testRunner/unittests/compileOnSave.ts rename to src/testRunner/unittests/tsserver/compileOnSave.ts index 4b98d71ef0a..7dc9ea0497c 100644 --- a/src/testRunner/unittests/compileOnSave.ts +++ b/src/testRunner/unittests/tsserver/compileOnSave.ts @@ -6,7 +6,7 @@ namespace ts.projectSystem { return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); } - describe("CompileOnSave affected list", () => { + describe("unittests:: tsserver:: compileOnSave:: affected list", () => { function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) { const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); @@ -502,9 +502,57 @@ namespace ts.projectSystem { ]); }); }); + + describe("tsserverProjectSystem emit with outFile or out setting", () => { + function test(opts: CompilerOptions, expectedUsesOutFile: boolean) { + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: opts, + compileOnSave: true + }) + }; + const host = createServerHost([f1, f2, config]); + const session = projectSystem.createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "open", + arguments: { file: f1.path } + }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + const { response } = session.executeCommand({ + seq: 2, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: f1.path } + }); + assert.equal((response).length, 1, "expected output for 1 project"); + assert.equal((response)[0].fileNames.length, 2, "expected output for 1 project"); + assert.equal((response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); + } + + it("projectUsesOutFile should not be returned if not set", () => { + test({}, /*expectedUsesOutFile*/ false); + }); + it("projectUsesOutFile should be true if outFile is set", () => { + test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); + }); + it("projectUsesOutFile should be true if out is set", () => { + test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); + }); + }); }); - describe("EmitFile test", () => { + describe("unittests:: tsserver:: compileOnSave:: EmitFile test", () => { it("should respect line endings", () => { test("\n"); test("\r\n"); @@ -646,4 +694,100 @@ namespace ts.projectSystem { } }); }); + + describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { + const projectRoot = "/user/username/projects/myproject"; + const core: File = { + path: `${projectRoot}/core/core.ts`, + content: "let z = 10;" + }; + const app1: File = { + path: `${projectRoot}/app1/app.ts`, + content: "let x = 10;" + }; + const app2: File = { + path: `${projectRoot}/app2/app.ts`, + content: "let y = 10;" + }; + const app1Config: File = { + path: `${projectRoot}/app1/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const app2Config: File = { + path: `${projectRoot}/app2/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const files = [libFile, core, app1, app2, app1Config, app2Config]; + + function insertString(session: TestSession, file: File) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: file.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: "let k = 1" + } + }); + } + + function getSession() { + const host = createServerHost(files); + const session = createSession(host); + openFilesForSession([app1, app2, core], session); + const service = session.getProjectService(); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + const project1 = service.configuredProjects.get(app1Config.path)!; + const project2 = service.configuredProjects.get(app2Config.path)!; + checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]); + checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]); + insertString(session, app1); + insertString(session, app2); + assert.equal(project1.dirty, true); + assert.equal(project2.dirty, true); + return session; + } + + it("when projectFile is specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path, + projectFileName: app1Config.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true); + }); + + it("when projectFile is not specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }, + { projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false); + }); + }); } diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts new file mode 100644 index 00000000000..0a62f74dc7d --- /dev/null +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -0,0 +1,122 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: completions", () => { + it("works", () => { + const aTs: File = { + path: "/a.ts", + content: "export const foo = 0;", + }; + const bTs: File = { + path: "/b.ts", + content: "foo", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + const session = createSession(createServerHost([aTs, bTs, tsconfig])); + openFilesForSession([aTs, bTs], session); + + const requestLocation: protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; + + const response = executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); + const entry: protocol.CompletionEntry = { + hasAction: true, + insertText: undefined, + isRecommended: undefined, + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + name: "foo", + replacementSpan: undefined, + sortText: "0", + source: "/a", + }; + assert.deepEqual(response, { + isGlobalCompletion: true, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [entry], + }); + + const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { + ...requestLocation, + entryNames: [{ name: "foo", source: "/a" }], + }; + + const detailsResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); + const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = { + displayParts: [ + keywordPart(SyntaxKind.ConstKeyword), + spacePart(), + displayPart("foo", SymbolDisplayPartKind.localName), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + displayPart("0", SymbolDisplayPartKind.stringLiteral), + ], + documentation: emptyArray, + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + name: "foo", + source: [{ text: "./a", kind: "text" }], + tags: undefined, + }; + assert.deepEqual | undefined>(detailsResponse, [ + { + codeActions: [ + { + description: `Import 'foo' from module "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: 'import { foo } from "./a";\n\n', + }, + ], + }, + ], + commands: undefined, + }, + ], + ...detailsCommon, + }, + ]); + + interface CompletionDetailsFullRequest extends protocol.FileLocationRequest { + readonly command: protocol.CommandTypes.CompletionDetailsFull; + readonly arguments: protocol.CompletionDetailsRequestArgs; + } + interface CompletionDetailsFullResponse extends protocol.Response { + readonly body?: ReadonlyArray; + } + const detailsFullResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); + assert.deepEqual | undefined>(detailsFullResponse, [ + { + codeActions: [ + { + description: `Import 'foo' from module "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], + }, + ], + commands: undefined, + } + ], + ...detailsCommon, + } + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/configFileSearch.ts b/src/testRunner/unittests/tsserver/configFileSearch.ts new file mode 100644 index 00000000000..c9e02b8bd06 --- /dev/null +++ b/src/testRunner/unittests/tsserver/configFileSearch.ts @@ -0,0 +1,174 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: searching for config file", () => { + it("should stop at projectRootPath if given", () => { + const f1 = { + path: "/a/file1.ts", + content: "" + }; + const configFile = { + path: "/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, configFile]); + const service = createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); + + checkNumberOfConfiguredProjects(service, 0); + checkNumberOfInferredProjects(service, 1); + + service.closeClientFile(f1.path); + service.openClientFile(f1.path); + checkNumberOfConfiguredProjects(service, 1); + checkNumberOfInferredProjects(service, 0); + }); + + it("should use projectRootPath when searching for inferred project again", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + const typeRootLocations = getTypeRootsFromLocation(configFileLocation); + checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project and not configured project + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); + }); + + it("should use projectRootPath when searching for inferred project again 2", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true }); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); + + // Delete config file - should create inferred project with project root path set + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.equal(service.inferredProjects[0].projectRootPath, projectDir); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); + }); + + describe("when the opened file is not from project root", () => { + const projectRoot = "/a/b/projects/project"; + const file: File = { + path: `${projectRoot}/src/index.ts`, + content: "let y = 10" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [file, libFile]; + const filesWithConfig = files.concat(tsconfig); + const dirOfFile = getDirectoryPath(file.path); + + function openClientFile(files: File[]) { + const host = createServerHost(files); + const projectService = createProjectService(host); + + projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); + return { host, projectService }; + } + + function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) { + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); + const project = Debug.assertDefined(projectService.configuredProjects.get(tsconfig.path)); + + if (orphanInferredProject) { + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.isOrphan()); + } + + checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); + checkWatchedFiles(host, [libFile.path, tsconfig.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); + } + + function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) { + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + assert.isDefined(project); + + const filesToWatch = [libFile.path]; + forEachAncestorDirectory(dirOfFile, ancestor => { + filesToWatch.push(combinePaths(ancestor, "tsconfig.json")); + filesToWatch.push(combinePaths(ancestor, "jsconfig.json")); + }); + + checkProjectActualFiles(project, [file.path, libFile.path]); + checkWatchedFiles(host, filesToWatch); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); + } + + it("tsconfig for the file exists", () => { + const { host, projectService } = openClientFile(filesWithConfig); + verifyConfiguredProject(host, projectService); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + }); + + it("tsconfig for the file does not exist", () => { + const { host, projectService } = openClientFile(files); + verifyInferredProject(host, projectService); + + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/configuredProjects.ts b/src/testRunner/unittests/tsserver/configuredProjects.ts new file mode 100644 index 00000000000..d1525f8dbeb --- /dev/null +++ b/src/testRunner/unittests/tsserver/configuredProjects.ts @@ -0,0 +1,1010 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: ConfiguredProjects", () => { + it("create configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "exclude": [ + "e" + ] + }` + }; + const file1: File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + const configFileDirectory = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); + }); + + it("create configured project with the file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "include": ["*.ts"] + }` + }; + const file1: File = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/c/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); + }); + + it("add and then remove a config file in a folder with loose files", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["commonFile1.ts"] + }` + }; + const filesWithoutConfig = [libFile, commonFile1, commonFile2]; + const host = createServerHost(filesWithoutConfig); + + const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + + const configFileLocations = ["/", "/a/", "/a/b/"]; + const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(libFile.path); + checkWatchedFiles(host, watchedFiles); + + // Add a tsconfig file + host.reloadFS(filesWithConfig); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + + projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [libFile.path, commonFile1.path, configFile.path]); + + checkWatchedFiles(host, watchedFiles); + + // remove the tsconfig file + host.reloadFS(filesWithoutConfig); + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + + host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects + + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + checkWatchedFiles(host, watchedFiles); + }); + + it("add new files to a configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + const configFileDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("handle recreated files correctly", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path]); + + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + + it("files explicitly excluded in config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + projectService.openClientFile(excludedFile1.path); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("should properly handle module resolution changes in config file", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const randomFile: File = { + path: "/a/file1.ts", + content: `export interface T {}` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(nodeModuleFile.path); + projectService.openClientFile(classicModuleFile.path); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + const inferredProject0 = projectService.inferredProjects[0]; + checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); + + configFile.content = `{ + "compilerOptions": { + "moduleResolution": "classic" + }, + "files": ["${file1.path}"] + }`; + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(2); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject1 = projectService.inferredProjects[1]; + checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + + // Open random file and it will reuse first inferred project + projectService.openClientFile(randomFile.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + }); + + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + }); + + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + }); + + it("should tolerate config file errors and still try to build a project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6", + "allowAnything": true + }, + "someOtherProperty": {} + }` + }; + const host = createServerHost([commonFile1, commonFile2, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); + }); + + it("should reuse same project if file is opened from the configured project that has no open files", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/main2.ts", + content: "let y =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts", "main2.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isFalse(project.hasOpenRef()); // No open files + assert.isFalse(project.isClosed()); + + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isTrue(project.hasOpenRef()); // file2 + assert.isFalse(project.isClosed()); + }); + + it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const host = createServerHost([file1, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + + projectService.openClientFile(libFile.path); + checkNumberOfConfiguredProjects(projectService, 0); + assert.isFalse(project.hasOpenRef()); // No files + project closed + assert.isTrue(project.isClosed()); + }); + + it("open file become a part of configured project if it is referenced from root file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const file2 = { + path: "/a/c/f2.ts", + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: "export let y = 1" + }; + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + host.reloadFS([file1, file2, file3, configFile]); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + + host.reloadFS([file1, file2, configFile]); + + host.checkTimeoutQueueLengthAndRun(2); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + + it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { + const file1: File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: File = { + path: "/a/file4.ts", + content: "let z = 1;" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) + }; + + const files = [file1, file2, file3, file4]; + const host = createServerHost(files.concat(configFile)); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + projectService.openClientFile(file4.path); + + const infos = files.map(file => projectService.getScriptInfoForPath(file.path as Path)!); + checkOpenFiles(projectService, files); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + const configProject1 = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 + checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject1, [file2.path]); + const inferredProject2 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject2, [file4.path]); + + configFile.content = "{}"; + host.reloadFS(files.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + + verifyScriptInfos(); + checkOpenFiles(projectService, files); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject3 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject3, [file4.path]); + assert.strictEqual(inferredProject3, inferredProject2); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file2.path); + projectService.closeClientFile(file4.path); + + verifyScriptInfos(); + checkOpenFiles(projectService, [file3]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + + projectService.openClientFile(file4.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file3, file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 + const inferredProject4 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + + projectService.closeClientFile(file3.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files + const inferredProject5 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + assert.strictEqual(inferredProject5, inferredProject4); + + const file5: File = { + path: "/file5.ts", + content: "let zz = 1;" + }; + host.reloadFS(files.concat(configFile, file5)); + projectService.openClientFile(file5.path); + verifyScriptInfosAreUndefined([file1, file2, file3]); + assert.strictEqual(projectService.getScriptInfoForPath(file4.path as Path), find(infos, info => info.path === file4.path)); + assert.isDefined(projectService.getScriptInfoForPath(file5.path as Path)); + checkOpenFiles(projectService, [file4, file5]); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); + + function verifyScriptInfos() { + infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); + } + + function verifyScriptInfosAreUndefined(files: File[]) { + for (const file of files) { + assert.isUndefined(projectService.getScriptInfoForPath(file.path as Path)); + } + } + + function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); + const configProject2 = projectService.configuredProjects.get(configFile.path)!; + assert.strictEqual(configProject2, configProject1); + checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); + assert.equal(configProject2.hasOpenRef(), hasOpenRef); + } + }); + + it("Open ref of configured project when open file gets added to the project as part of configured file update buts its open file references are all closed when the update happens", () => { + const file1: File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: File = { + path: "/a/file4.ts", + content: "let z = 1;" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) + }; + + const files = [file1, file2, file3]; + const hostFiles = files.concat(file4, configFile); + const host = createServerHost(hostFiles); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const configuredProject = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 + checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject1, [file2.path]); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file3.path); + assert.isFalse(configuredProject.hasOpenRef()); // No files + + configFile.content = "{}"; + host.reloadFS(files.concat(configFile)); + // Time out is not yet run so there is project update pending + assert.isTrue(configuredProject.hasOpenRef()); // Pending update and file2 might get into the project + + projectService.openClientFile(file4.path); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // Pending update and F2 might get into the project + assert.strictEqual(projectService.inferredProjects[0], inferredProject1); + const inferredProject2 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject2, [file4.path]); + + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // file2 + checkProjectActualFiles(configuredProject, [file1.path, file2.path, file3.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + assert.strictEqual(projectService.inferredProjects[1], inferredProject2); + checkProjectActualFiles(inferredProject2, [file4.path]); + }); + + it("files are properly detached when language service is disabled", () => { + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const f3 = { + path: "/a/lib.js", + content: "var x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([f1, f2, f3, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + assert.isTrue(project.hasOpenRef()); // f1 + assert.isFalse(project.isClosed()); + + projectService.closeClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + + for (const f of [f1, f2, f3]) { + // All the script infos should be present and contain the project since it is still alive. + const scriptInfo = projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))!; + assert.equal(scriptInfo.containingProjects.length, 1, `expect 1 containing projects for '${f.path}'`); + assert.equal(scriptInfo.containingProjects[0], project, `expect configured project to be the only containing project for '${f.path}'`); + } + + const f4 = { + path: "/aa.js", + content: "var x = 1" + }; + host.reloadFS([f1, f2, f3, config, f4]); + projectService.openClientFile(f4.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isFalse(project.hasOpenRef()); // No files + assert.isTrue(project.isClosed()); + + for (const f of [f1, f2, f3]) { + // All the script infos should not be present since the project is closed and orphan script infos are collected + assert.isUndefined(projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))); + } + }); + + it("syntactic features work even if language service is disabled", () => { + const f1 = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const { session, events } = createSessionWithEventTracking(host, server.ProjectLanguageServiceStateEvent); + session.executeCommand({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + assert.isFalse(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 1, "should receive event"); + assert.equal(events[0].data.project, project, "project name"); + assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); + + const options = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); + const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); + assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); + }); + }); + + describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => { + it("should be tolerated without crashing the server", () => { + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "include": ["app/*", "test/**/*", "something"] + }` + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + host.runQueuedTimeoutCallbacks(); + // Since there is no file open from configFile it would be closed + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.containsFile(file1.path)); + }); + + it("should be able to handle @types if input file list is empty", () => { + const f = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + const host = createServerHost([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); + const projectService = createProjectService(host); + + projectService.openClientFile(f.path); + // Since no file from the configured project is open, it would be closed immediately + projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 }); + }); + + it("should tolerate invalid include files that start in subDirectory", () => { + const projectFolder = "/user/username/projects/myproject"; + const f = { + path: `${projectFolder}/src/server/index.ts`, + content: "let x = 1" + }; + const config = { + path: `${projectFolder}/src/server/tsconfig.json`, + content: JSON.stringify({ + compiler: { + module: "commonjs", + outDir: "../../build" + }, + include: [ + "../src/**/*.ts" + ] + }) + }; + const host = createServerHost([f, config, libFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + + projectService.openClientFile(f.path); + // Since no file from the configured project is open, it would be closed immediately + projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 }); + }); + + it("Changed module resolution reflected when specifying files list", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: 'import classc from "file2"' + }; + const file2a: File = { + path: "/a/file2.ts", + content: "export classc { method2a() { return 10; } }" + }; + const file2: File = { + path: "/a/b/file2.ts", + content: "export classc { method2() { return 10; } }" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) + }; + const files = [file1, file2a, configFile, libFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + checkProjectActualFiles(project, map(files, file => file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + + files.push(file2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + + // On next file open the files file2a should be closed and not watched any more + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + }); + + it("Failed lookup locations uses parent most node_modules directory", () => { + const root = "/user/username/rootfolder"; + const file1: File = { + path: "/a/b/src/file1.ts", + content: 'import { classc } from "module1"' + }; + const module1: File = { + path: "/a/b/node_modules/module1/index.d.ts", + content: `import { class2 } from "module2"; + export classc { method2a(): class2; }` + }; + const module2: File = { + path: "/a/b/node_modules/module2/index.d.ts", + content: "export class2 { method2() { return 10; } }" + }; + const module3: File = { + path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", + content: "export class3 { method2() { return 10; } }" + }; + const configFile: File = { + path: "/a/b/src/tsconfig.json", + content: JSON.stringify({ files: ["file1.ts"] }) + }; + const nonLibFiles = [file1, module1, module2, module3, configFile]; + nonLibFiles.forEach(f => f.path = root + f.path); + const files = nonLibFiles.concat(libFile); + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); + watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts new file mode 100644 index 00000000000..200d7af0240 --- /dev/null +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -0,0 +1,566 @@ +namespace ts.projectSystem { + function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan { + return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) }; + } + + function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan { + return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) }; + } + + function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation { + return documentSpanFromSubstring(file, substring, options); + } + + function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem { + return { + ...protocolFileSpanFromSubstring(file, text, options), + isDefinition, + isWriteAccess: isDefinition, + lineText, + }; + } + + function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry { + return { + ...documentSpanFromSubstring(file, text, options), + isDefinition, + isWriteAccess: isDefinition, + isInString: undefined, + }; + } + + function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray): void { + openFilesForSession([file], session); + const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); + const program = project.getCurrentProgram()!; + const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); + closeFilesForSession([file], session); + + Debug.assert(!output.emitSkipped); + assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); + } + + describe("unittests:: tsserver:: with declaration file maps:: project references", () => { + const aTs: File = { + path: "/a/a.ts", + content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", + }; + const compilerOptions: CompilerOptions = { + outDir: "bin", + declaration: true, + declarationMap: true, + composite: true, + }; + const configContent = JSON.stringify({ compilerOptions }); + const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent }; + + const aDtsMapContent: RawSourceMap = { + version: 3, + file: "a.d.ts", + sourceRoot: "", + sources: ["../a.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC" + }; + const aDtsMap: File = { + path: "/a/bin/a.d.ts.map", + content: JSON.stringify(aDtsMapContent), + }; + const aDts: File = { + path: "/a/bin/a.d.ts", + // Need to mangle the sourceMappingURL part or it breaks the build + content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`, + }; + + const bTs: File = { + path: "/b/b.ts", + content: "export function fnB() {}", + }; + const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent }; + + const bDtsMapContent: RawSourceMap = { + version: 3, + file: "b.d.ts", + sourceRoot: "", + sources: ["../b.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK", + }; + const bDtsMap: File = { + path: "/b/bin/b.d.ts.map", + content: JSON.stringify(bDtsMapContent), + }; + const bDts: File = { + // Need to mangle the sourceMappingURL part or it breaks the build + path: "/b/bin/b.d.ts", + content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, + }; + + const dummyFile: File = { + path: "/dummy/dummy.ts", + content: "let a = 10;" + }; + + const userTs: File = { + path: "/user/user.ts", + content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', + }; + + const userTsForConfigProject: File = { + path: "/user/user.ts", + content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', + }; + + const userTsconfig: File = { + path: "/user/tsconfig.json", + content: JSON.stringify({ + file: ["user.ts"], + references: [{ path: "../a" }, { path: "../b" }] + }) + }; + + function makeSampleProjects(addUserTsConfig?: boolean) { + const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); + const session = createSession(host); + + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); + + // Testing what happens if we delete the original sources. + host.deleteFile(bTs.path); + + openFilesForSession([userTs], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 1 } : { inferredProjects: 1 }); + return session; + } + + function verifyInferredProjectUnchanged(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); + } + + function verifyDummyProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); + } + + function verifyOnlyOrphanInferredProject(session: TestSession) { + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyDummyProject(session); + } + + function verifySingleInferredProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyInferredProjectUnchanged(session); + + // Close user file should close all the projects after opening dummy file + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + + function verifyATsConfigProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); + } + + function verifyATsConfigOriginalProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + // Close user file should close all the projects + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + + function verifyATsConfigWhenOpened(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + + closeFilesForSession([userTs], session); + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyDummyProject(session); + verifyATsConfigProject(session); // ATsConfig should still be alive + } + + function verifyUserTsConfigProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aDts.path, userTsconfig.path]); + } + + it("goToDefinition", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); + verifySingleInferredProject(session); + }); + + it("getDefinitionAndBoundSpan", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], + }); + verifySingleInferredProject(session); + }); + + it("getDefinitionAndBoundSpan with file navigation", () => { + const session = makeSampleProjects(/*addUserTsConfig*/ true); + const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], + }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + verifyUserTsConfigProject(session); + + // Navigate to the definition + closeFilesForSession([userTs], session); + openFilesForSession([aTs], session); + + // UserTs configured project should be alive + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + verifyUserTsConfigProject(session); + verifyATsConfigProject(session); + + closeFilesForSession([aTs], session); + verifyOnlyOrphanInferredProject(session); + }); + + it("goToType", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA")); + assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "IfaceA")]); + verifySingleInferredProject(session); + }); + + it("goToImplementation", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]); + verifySingleInferredProject(session); + }); + + it("goToDefinition -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()")); + // bTs does not exist, so stick with bDts + assert.deepEqual(response, [protocolFileSpanFromSubstring(bDts, "fnB")]); + verifySingleInferredProject(session); + }); + + it("navigateTo", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); + assert.deepEqual | undefined>(response, [ + { + ...protocolFileSpanFromSubstring(bDts, "export declare function fnB(): void;"), + name: "fnB", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export,declare", + }, + { + ...protocolFileSpanFromSubstring(userTs, "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), + name: "fnUser", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + }, + { + ...protocolFileSpanFromSubstring(aTs, "export function fnA() {}"), + name: "fnA", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + }, + ]); + + verifyATsConfigOriginalProject(session); + }); + + const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem(aTs, /*isDefinition*/ true, "fnA", "export function fnA() {}"); + const referencesUserTs = (userTs: File): ReadonlyArray => [ + makeReferenceItem(userTs, /*isDefinition*/ false, "fnA", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), + ]; + + it("findAllReferences", () => { + const session = makeSampleProjects(); + + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + refs: [...referencesUserTs(userTs), referenceATs(aTs)], + symbolName: "fnA", + symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset, + symbolDisplayString: "function fnA(): void", + }); + + verifyATsConfigOriginalProject(session); + }); + + it("findAllReferences -- starting at definition", () => { + const session = makeSampleProjects(); + openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + refs: [referenceATs(aTs), ...referencesUserTs(userTs)], + symbolName: "fnA", + symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset, + symbolDisplayString: "function fnA(): void", + }); + verifyATsConfigWhenOpened(session); + }); + + interface ReferencesFullRequest extends protocol.FileLocationRequest { readonly command: protocol.CommandTypes.ReferencesFull; } + interface ReferencesFullResponse extends protocol.Response { readonly body: ReadonlyArray; } + + it("findAllReferencesFull", () => { + const session = makeSampleProjects(); + + const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()")); + + assert.deepEqual>(responseFull, [ + { + definition: { + ...documentSpanFromSubstring(aTs, "fnA"), + kind: ScriptElementKind.functionElement, + name: "function fnA(): void", + containerKind: ScriptElementKind.unknown, + containerName: "", + displayParts: [ + keywordPart(SyntaxKind.FunctionKeyword), + spacePart(), + displayPart("fnA", SymbolDisplayPartKind.functionName), + punctuationPart(SyntaxKind.OpenParenToken), + punctuationPart(SyntaxKind.CloseParenToken), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + keywordPart(SyntaxKind.VoidKeyword), + ], + }, + references: [ + makeReferenceEntry(userTs, /*isDefinition*/ false, "fnA"), + makeReferenceEntry(aTs, /*isDefinition*/ true, "fnA"), + ], + }, + ]); + verifyATsConfigOriginalProject(session); + }); + + it("findAllReferencesFull definition is in mapped file", () => { + const aTs: File = { path: "/a/a.ts", content: `function f() {}` }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), + }; + const bTs: File = { path: "/b/b.ts", content: `f();` }; + const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; + const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; + const aDtsMap: File = { + path: "/bin/a.d.ts.map", + content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }), + }; + + const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + openFilesForSession([bTs], session); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + + const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()")); + + assert.deepEqual>(responseFull, [ + { + definition: { + containerKind: ScriptElementKind.unknown, + containerName: "", + displayParts: [ + keywordPart(SyntaxKind.FunctionKeyword), + spacePart(), + displayPart("f", SymbolDisplayPartKind.functionName), + punctuationPart(SyntaxKind.OpenParenToken), + punctuationPart(SyntaxKind.CloseParenToken), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + keywordPart(SyntaxKind.VoidKeyword), + ], + fileName: aTs.path, + kind: ScriptElementKind.functionElement, + name: "function f(): void", + textSpan: { start: 9, length: 1 }, + }, + references: [ + { + fileName: bTs.path, + isDefinition: false, + isInString: undefined, + isWriteAccess: false, + textSpan: { start: 0, length: 1 }, + }, + { + fileName: aTs.path, + isDefinition: true, + isInString: undefined, + isWriteAccess: true, + textSpan: { start: 9, length: 1 }, + }, + ], + } + ]); + }); + + it("findAllReferences -- target does not exist", () => { + const session = makeSampleProjects(); + + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + refs: [ + makeReferenceItem(bDts, /*isDefinition*/ true, "fnB", "export declare function fnB(): void;"), + makeReferenceItem(userTs, /*isDefinition*/ false, "fnB", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"), + ], + symbolName: "fnB", + symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset, + symbolDisplayString: "function fnB(): void", + }); + verifySingleInferredProject(session); + }); + + const renameATs = (aTs: File): protocol.SpanGroup => ({ + file: aTs.path, + locs: [protocolRenameSpanFromSubstring(aTs.content, "fnA")], + }); + const renameUserTs = (userTs: File): protocol.SpanGroup => ({ + file: userTs.path, + locs: [protocolRenameSpanFromSubstring(userTs.content, "fnA")], + }); + + it("renameLocations", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/bin/a".fnA', // Ideally this would use the original source's path instead of the declaration file's path. + kind: ScriptElementKind.functionElement, + kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + }, + locs: [renameUserTs(userTs), renameATs(aTs)], + }); + verifyATsConfigOriginalProject(session); + }); + + it("renameLocations -- starting at definition", () => { + const session = makeSampleProjects(); + openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/a".fnA', + kind: ScriptElementKind.functionElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"), + }, + locs: [renameATs(aTs), renameUserTs(userTs)], + }); + verifyATsConfigWhenOpened(session); + }); + + it("renameLocationsFull", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual>(response, [ + renameLocation(userTs, "fnA"), + renameLocation(aTs, "fnA"), + ]); + verifyATsConfigOriginalProject(session); + }); + + it("renameLocations -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnB", + fileToRename: undefined, + fullDisplayName: '"/b/bin/b".fnB', + kind: ScriptElementKind.functionElement, + kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), + }, + locs: [ + { + file: bDts.path, + locs: [protocolRenameSpanFromSubstring(bDts.content, "fnB")], + }, + { + file: userTs.path, + locs: [protocolRenameSpanFromSubstring(userTs.content, "fnB")], + }, + ], + }); + verifySingleInferredProject(session); + }); + + it("getEditsForFileRename", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/aNew.ts", + }); + assert.deepEqual>(response, [ + { + fileName: userTs.path, + textChanges: [ + { ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" }, + ], + }, + ]); + verifySingleInferredProject(session); + }); + + it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => { + const aTs: File = { path: "/a/src/a.ts", content: "" }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: true, + outDir: "./build", + } + }), + }; + const bTs: File = { path: "/b/src/b.ts", content: "" }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./build", + }, + include: ["./src"], + references: [{ path: "../a" }], + }), + }; + + const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aTs, bTs], session); + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/src/a1.ts", + }); + assert.deepEqual>(response, []); // Should not change anything + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/documentRegistry.ts b/src/testRunner/unittests/tsserver/documentRegistry.ts new file mode 100644 index 00000000000..1761e413833 --- /dev/null +++ b/src/testRunner/unittests/tsserver/documentRegistry.ts @@ -0,0 +1,94 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: document registry in project service", () => { + const projectRootPath = "/user/username/projects/project"; + const importModuleContent = `import {a} from "./module1"`; + const file: File = { + path: `${projectRootPath}/index.ts`, + content: importModuleContent + }; + const moduleFile: File = { + path: `${projectRootPath}/module1.d.ts`, + content: "export const a: number;" + }; + const configFile: File = { + path: `${projectRootPath}/tsconfig.json`, + content: JSON.stringify({ files: ["index.ts"] }) + }; + + function getProject(service: TestProjectService) { + return service.configuredProjects.get(configFile.path)!; + } + + function checkProject(service: TestProjectService, moduleIsOrphan: boolean) { + // Update the project + const project = getProject(service); + project.getLanguageService(); + checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]); + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + assert.isDefined(moduleInfo); + assert.equal(moduleInfo.isOrphan(), moduleIsOrphan); + const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings()); + assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]); + } + + function createServiceAndHost() { + const host = createServerHost([file, moduleFile, libFile, configFile]); + const service = createProjectService(host); + service.openClientFile(file.path); + checkProject(service, /*moduleIsOrphan*/ false); + return { host, service }; + } + + function changeFileToNotImportModule(service: TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]); + checkProject(service, /*moduleIsOrphan*/ true); + } + + function changeFileToImportModule(service: TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]); + checkProject(service, /*moduleIsOrphan*/ false); + } + + it("Caches the source file if script info is orphan", () => { + const { service } = createServiceAndHost(); + const project = getProject(service); + + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + + // write content back + changeFileToImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + }); + + it("Caches the source file if script info is orphan, and orphan script info changes", () => { + const { host, service } = createServiceAndHost(); + const project = getProject(service); + + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + + const updatedModuleContent = moduleFile.content + "\nexport const b: number;"; + host.writeFile(moduleFile.path, updatedModuleContent); + + // write content back + changeFileToImportModule(service); + assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/duplicatePackages.ts b/src/testRunner/unittests/tsserver/duplicatePackages.ts new file mode 100644 index 00000000000..ec85eb6a82f --- /dev/null +++ b/src/testRunner/unittests/tsserver/duplicatePackages.ts @@ -0,0 +1,54 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: duplicate packages", () => { + // Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier. + it("works with import fixes", () => { + const packageContent = "export const foo: number;"; + const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" }); + const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; + const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; + const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; + const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; + + const userContent = 'import("foo");\nfoo'; + const aUser: File = { path: "/a/user.ts", content: userContent }; + const bUser: File = { path: "/b/user.ts", content: userContent }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); + const session = createSession(host); + + openFilesForSession([aUser, bUser], session); + + for (const user of [aUser, bUser]) { + const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { + file: user.path, + startLine: 2, + startOffset: 1, + endLine: 2, + endOffset: 4, + errorCodes: [Diagnostics.Cannot_find_name_0.code], + }); + assert.deepEqual | undefined>(response, [ + { + description: `Import 'foo' from module "foo"`, + fixName: "import", + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + changes: [{ + fileName: user.path, + textChanges: [{ + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: 'import { foo } from "foo";\n\n', + }], + }], + commands: undefined, + }, + ]); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts new file mode 100644 index 00000000000..ff69a7e6568 --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts @@ -0,0 +1,76 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => { + const projectRoot = "/user/username/projects/project"; + + function getLargeFile(useLargeTsFile: boolean) { + return `src/large.${useLargeTsFile ? "ts" : "js"}`; + } + + function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) { + const largeFile: File = { + path: `${projectRoot}/${getLargeFile(useLargeTsFile)}`, + content: "export var x = 10;", + fileSize: server.maxFileSize + 1 + }; + files.push(largeFile); + const host = createServerHost(files); + const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking(host, server.LargeFileReferencedEvent); + + return { session, verifyLargeFile }; + + function verifyLargeFile(project: server.Project) { + checkProjectActualFiles(project, files.map(f => f.path)); + + // large file for non ts file should be empty and for ts file should have content + const service = session.getProjectService(); + const info = service.getScriptInfo(largeFile.path)!; + assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : ""); + + assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? emptyArray : [{ + eventName: server.LargeFileReferencedEvent, + data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: server.maxFileSize } + }]); + } + } + + function verifyLargeFile(useLargeTsFile: boolean) { + it("when large file is included by tsconfig", () => { + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) + }; + const files = [file, libFile, tsconfig]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + verifyLargeFile(service.configuredProjects.get(tsconfig.path)!); + }); + + it("when large file is included by module resolution", () => { + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: `export var y = 10;import {x} from "./large"` + }; + const files = [file, libFile]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + verifyLargeFile(service.inferredProjects[0]); + }); + } + + describe("large file is ts file", () => { + verifyLargeFile(/*useLargeTsFile*/ true); + }); + + describe("large file is js file", () => { + verifyLargeFile(/*useLargeTsFile*/ false); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts new file mode 100644 index 00000000000..231c46c350b --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts @@ -0,0 +1,51 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", () => { + it("language service disabled events are triggered", () => { + const f1 = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const configWithExclude = { + path: config.path, + content: JSON.stringify({ exclude: ["largefile.js"] }) + }; + const host = createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const { session, events } = createSessionWithEventTracking(host, server.ProjectLanguageServiceStateEvent); + session.executeCommand({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + }); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + assert.isFalse(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 1, "should receive event"); + assert.equal(events[0].data.project, project, "project name"); + assert.equal(events[0].data.project.getProjectName(), config.path, "config path"); + assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); + + host.reloadFS([f1, f2, configWithExclude]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isTrue(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 2, "should receive event"); + assert.equal(events[1].data.project, project, "project"); + assert.equal(events[1].data.project.getProjectName(), config.path, "config path"); + assert.isTrue(events[1].data.languageServiceEnabled, "Language service state"); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/projectLoading.ts b/src/testRunner/unittests/tsserver/events/projectLoading.ts new file mode 100644 index 00000000000..7a881ff1380 --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/projectLoading.ts @@ -0,0 +1,193 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { + const projectRoot = "/user/username/projects"; + const aTs: File = { + path: `${projectRoot}/a/a.ts`, + content: "export class A { }" + }; + const configA: File = { + path: `${projectRoot}/a/tsconfig.json`, + content: "{}" + }; + const bTsPath = `${projectRoot}/b/b.ts`; + const configBPath = `${projectRoot}/b/tsconfig.json`; + const files = [libFile, aTs, configA]; + + function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => { + session: TestSession; + getNumberOfEvents: () => number; + clearEvents: () => void; + verifyProjectLoadEvents: (expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) => void; + }) { + function createSessionToVerifyEvent(files: ReadonlyArray) { + const host = createServerHost(files); + const originalReadFile = host.readFile; + const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host); + host.readFile = file => { + if (file === configA.path || file === configBPath) { + assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file"); + } + return originalReadFile.call(host, file); + }; + const service = session.getProjectService(); + return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents }; + + function verifyEvent(project: server.Project, reason: string) { + verifyProjectLoadEvents([ + { eventName: server.ProjectLoadingStartEvent, data: { project, reason } }, + { eventName: server.ProjectLoadingFinishEvent, data: { project } } + ]); + clearEvents(); + } + + function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) { + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects }); + const project = service.configuredProjects.get(configPath)!; + assert.isDefined(project); + verifyEvent(project, `Creating possible configured project for ${file.path} to open`); + } + } + + it("when project is created by open file", () => { + const bTs: File = { + path: bTsPath, + content: "export class B {}" + }; + const configB: File = { + path: configBPath, + content: "{}" + }; + const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB)); + verifyEventWithOpenTs(aTs, configA.path, 1); + verifyEventWithOpenTs(bTs, configB.path, 2); + }); + + it("when change is detected in the config file", () => { + const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files); + verifyEventWithOpenTs(aTs, configA.path, 1); + + host.writeFile(configA.path, configA.content); + host.checkTimeoutQueueLengthAndRun(2); + const project = service.configuredProjects.get(configA.path)!; + verifyEvent(project, `Change in config file detected`); + }); + + it("when opening original location project", () => { + const aDTs: File = { + path: `${projectRoot}/a/a.d.ts`, + content: `export declare class A { +} +//# sourceMappingURL=a.d.ts.map +` + }; + const aDTsMap: File = { + path: `${projectRoot}/a/a.d.ts.map`, + content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` + }; + const bTs: File = { + path: bTsPath, + content: `import {A} from "../a/a"; new A();` + }; + const configB: File = { + path: configBPath, + content: JSON.stringify({ + references: [{ path: "../a" }] + }) + }; + + const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); + verifyEventWithOpenTs(bTs, configB.path, 1); + + session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: { + file: bTs.path, + ...protocolLocationFromSubstring(bTs.content, "A()") + } + }); + + checkNumberOfProjects(service, { configuredProjects: 2 }); + const project = service.configuredProjects.get(configA.path)!; + assert.isDefined(project); + verifyEvent(project, `Creating project for original file: ${aTs.path} for location: ${aDTs.path}`); + }); + + describe("with external projects and config files ", () => { + const projectFileName = `${projectRoot}/a/project.csproj`; + + function createSession(lazyConfiguredProjectsFromExternalProject: boolean) { + const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([aTs.path, configA.path]), + options: {} + }); + checkNumberOfProjects(service, { configuredProjects: 1 }); + return { session, service, verifyEvent, getNumberOfEvents }; + + function verifyEvent() { + const projectA = service.configuredProjects.get(configA.path)!; + assert.isDefined(projectA); + verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`); + } + } + + it("when lazyConfiguredProjectsFromExternalProject is false", () => { + const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false); + verifyEvent(); + }); + + it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => { + const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); + + openFilesForSession([aTs], session); + verifyEvent(); + }); + + it("when lazyConfiguredProjectsFromExternalProject is disabled", () => { + const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); + + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + verifyEvent(); + }); + }); + } + + describe("when using event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, events } = createSessionWithEventTracking(host, server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent); + return { + session, + getNumberOfEvents: () => events.length, + clearEvents: () => events.length = 0, + verifyProjectLoadEvents: expected => assert.deepEqual(events, expected) + }; + }); + }); + + describe("when using default event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]); + return { + session, + getNumberOfEvents: () => getEvents().length, + clearEvents, + verifyProjectLoadEvents + }; + + function verifyProjectLoadEvents(expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) { + const actual = getEvents().map(e => ({ eventName: e.event, data: e.body })); + const mappedExpected = expected.map(e => { + const { project, ...rest } = e.data; + return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } }; + }); + assert.deepEqual(actual, mappedExpected); + } + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts new file mode 100644 index 00000000000..05a43cb4a15 --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts @@ -0,0 +1,591 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => { + function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); + const seen = createMap(); + forEach(actual, f => { + assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); + seen.set(f, true); + assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); + }); + } + + function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) { + return (file: File) => { + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: file.path + } + }); + verifyProjectsUpdatedInBackgroundEventHandler([]); + }; + } + + interface ProjectsUpdatedInBackgroundEventVerifier { + session: TestSession; + verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void; + verifyInitialOpen(file: File): void; + } + + function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { + it("when adding new file", () => { + const commonFile1: File = { + path: "/a/b/file1.ts", + content: "export var x = 10;" + }; + const commonFile2: File = { + path: "/a/b/file2.ts", + content: "export var y = 10;" + }; + const commonFile3: File = { + path: "/a/b/file3.ts", + content: "export var z = 10;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const openFiles = [commonFile1.path]; + const host = createServerHost([commonFile1, libFile, configFile]); + const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + verifyInitialOpen(commonFile1); + + host.reloadFS([commonFile1, libFile, configFile, commonFile2]); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + }); + + describe("with --out or --outFile setting", () => { + function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { + const config: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions + }) + }; + + const f1: File = { + path: "/a/a.ts", + content: "export let x = 1" + }; + const f2: File = { + path: "/a/b.ts", + content: "export let y = 1" + }; + + const openFiles = [f1.path]; + const files = [f1, config, libFile]; + const host = createServerHost(files); + const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + verifyInitialOpen(f1); + + files.push(f2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + f2.content = "export let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + } + + it("when both options are not set", () => { + verifyEventWithOutSettings(); + }); + + it("when --out is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ out: outJs }); + }); + + it("when --outFile is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ outFile: outJs }); + }); + }); + + describe("with modules and configured project", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const configFilePath = "/a/b/tsconfig.json"; + interface InitialStateParams { + /** custom config file options */ + configObj?: any; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): File[]; + /** initial list of files to reload in fs and first file in this list being the file to open */ + firstReloadFileList?: string[]; + } + function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { + const moduleFile1: File = { + path: moduleFile1Path, + content: "export function Foo() { };", + }; + + const file1Consumer1: File = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }; + + const file1Consumer2: File = { + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }; + + const moduleFile2: File = { + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }; + + const globalFile3: File = { + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }; + + const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; + const configFile = { + path: configFilePath, + content: JSON.stringify(configObj || { compilerOptions: {} }) + }; + + const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; + + const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; + const host = createServerHost([filesToReload[0], configFile]); + + // Initial project creation + const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + const openFiles = [filesToReload[0].path]; + verifyInitialOpen(filesToReload[0]); + + // Since this is first event, it will have all the files + verifyProjectsUpdatedInBackgroundEvent(filesToReload); + + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + updateContentOfOpenFile, + verifyNoProjectsUpdatedInBackgroundEvent, + verifyProjectsUpdatedInBackgroundEvent + }; + + function getFiles(filelist: string[]) { + return map(filelist, getFile); + } + + function getFile(fileName: string) { + return find(files, file => file.path === fileName)!; + } + + function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([]); + } + + function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + } + + function updateContentOfOpenFile(file: File, newContent: string) { + session.executeCommandSeq({ + command: server.CommandNames.Change, + arguments: { + file: file.path, + insertString: newContent, + endLine: 1, + endOffset: file.content.length, + line: 1, + offset: 1 + } + }); + file.content = newContent; + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should be up-to-date with the reference map changes", () => { + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); + + // Change file1Consumer1 content to `export let y = Foo();` + updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Add the import statements back to file1Consumer1 + updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); + verifyNoProjectsUpdatedInBackgroundEvent(); + + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Multiple file edits in one go: + + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should be up-to-date with deleted files", () => { + const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + + // Delete file1Consumer2 + const filesToLoad = filter(files, file => file !== file1Consumer2); + verifyProjectsUpdatedInBackgroundEvent(filesToLoad); + }); + + it("should be up-to-date with newly created files", () => { + const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); + + const file1Consumer3: File = { + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3)); + }); + + it("should detect changes in non-root files", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { files: [file1Consumer1Path] }, + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should return all files if a global file changed shape", () => { + const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + + globalFile3.content += "var T2: string;"; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { isolatedModules: true } } + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } } + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] + }); + + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); + verifyNoProjectsUpdatedInBackgroundEvent(); + + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + + // Change both files before the timeout + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + + it("should work fine for files with circular references", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: ` + /// + export var t1 = 10;` + }; + const file2: File = { + path: "/a/b/file2.ts", + content: ` + /// + export var t2 = 10;` + }; + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] + }); + + file2.content += "export var t3 = 10;"; + verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]); + }); + + it("should detect removed code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] + }); + + verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] + }); + + updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + + // Create module File2 and see both files are saved + verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]); + }); + }); + + describe("resolution when resolution cache size", () => { + function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { + const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; + const file1: File = { + path: rootFolder + "a/b/project/file1.ts", + content: 'import a from "file2"' + }; + const file2: File = { + path: rootFolder + "a/b/node_modules/file2.d.ts", + content: "export class a { }" + }; + const file3: File = { + path: rootFolder + "a/b/project/file3.ts", + content: "export class c { }" + }; + const configFile: File = { + path: rootFolder + "a/b/project/tsconfig.json", + content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) + }; + + const projectFiles = [file1, file3, libFile, configFile]; + const openFiles = [file1.path]; + const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? + // Folders of node_modules lookup not in changedRoot + ["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : + // Folder of tsconfig + ["/a/b/project", "/a/b/project/node_modules"]; + const host = createServerHost(projectFiles); + const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(file1); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + verifyProject(); + if (limitHit) { + (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; + } + + file3.content += "export class d {}"; + host.reloadFS(projectFiles); + host.checkTimeoutQueueLengthAndRun(2); + + // Since this is first event + verifyProject(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); + + projectFiles.push(file2); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + if (useSlashRootAsSomeNotRootFolderInUserDirectory) { + watchedRecursiveDirectories.length = 3; + } + else { + // file2 addition wont be detected + projectFiles.pop(); + assert.isTrue(host.fileExists(file2.path)); + } + verifyProject(); + + verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }] : []); + + function verifyProject() { + checkProjectActualFiles(project, map(projectFiles, file => file.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + } + } + + it("limit not hit and project is not at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); + }); + + it("limit hit and project is not at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); + }); + + it("limit not hit and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); + }); + + it("limit hit and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); + }); + }); + } + + describe("when event handler is set in the session", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); + + function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { + const { session, events: projectChangedEvents } = createSessionWithEventTracking(host, server.ProjectsUpdatedInBackgroundEvent); + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; + + function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) { + return JSON.stringify(event && { eventName: event.eventName, data: event.data }); + } + + function eventsToString(events: ReadonlyArray) { + return "[" + map(events, eventToString).join(",") + "]"; + } + + function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: ReadonlyArray) { + assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); + forEach(projectChangedEvents, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); + verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); + }); + + // Verified the events, reset them + projectChangedEvents.length = 0; + } + } + }); + + describe("when event handler is not set but session is created with canUseEvents = true", () => { + describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents); + }); + + describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { + verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true)); + }); + + + function createSessionThatUsesEvents(host: TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { + const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); + + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; + + function verifyProjectsUpdatedInBackgroundEventHandler(expected: ReadonlyArray) { + const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { + return { + openFiles: e.data.openFiles + }; + }); + const events = getEvents(); + assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); + forEach(events, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); + }); + + // Verified the events, reset them + clearEvents(); + + if (events.length) { + host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate + } + } + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/events/surveyReady.ts b/src/testRunner/unittests/tsserver/events/surveyReady.ts new file mode 100644 index 00000000000..b04746800d0 --- /dev/null +++ b/src/testRunner/unittests/tsserver/events/surveyReady.ts @@ -0,0 +1,111 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: events:: SurveyReady", () => { + function createSessionWithEventHandler(host: TestServerHost) { + const { session, events: surveyEvents } = createSessionWithEventTracking(host, server.SurveyReady); + + return { session, verifySurveyReadyEvent }; + + function verifySurveyReadyEvent(numberOfEvents: number) { + assert.equal(surveyEvents.length, numberOfEvents); + const expectedEvents = numberOfEvents === 0 ? [] : [{ + eventName: server.SurveyReady, + data: { surveyId: "checkJs" } + }]; + assert.deepEqual(surveyEvents, expectedEvents); + } + } + + it("doesn't log an event when checkJs isn't set", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, [file.path, tsconfig.path]); + + verifySurveyReadyEvent(0); + }); + + it("logs an event when checkJs is set", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { checkJs: true } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + + it("logs an event when checkJs is set, only the first time", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const rando: File = { + path: `/rando/calrissian.ts`, + content: "export function f() { }" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { checkJs: true } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + + closeFilesForSession([file], session); + openFilesForSession([rando], session); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + + it("logs an event when checkJs is set after closing and reopening", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const rando: File = { + path: `/rando/calrissian.ts`, + content: "export function f() { }" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({}), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(0); + + closeFilesForSession([file], session); + openFilesForSession([rando], session); + host.writeFile(tsconfig.path, JSON.stringify({ compilerOptions: { checkJs: true } })); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts new file mode 100644 index 00000000000..2055141538a --- /dev/null +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -0,0 +1,822 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: ExternalProjects", () => { + describe("can handle tsconfig file name with difference casing", () => { + function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + include: [] + }) + }; + + const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); + const service = createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); + service.openExternalProject({ + projectFileName: "/a/b/project.csproj", + rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + if (lazyConfiguredProjectsFromExternalProject) { + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + checkProjectActualFiles(project, emptyArray); + } + else { + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project, [upperCaseConfigFilePath]); + } + + service.openClientFile(f1.path); + service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated + checkProjectActualFiles(project, [upperCaseConfigFilePath]); + checkProjectActualFiles(service.inferredProjects[0], [f1.path]); + } + + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + it("remove not-listed external projects", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const f2 = { + path: "/b/app.ts", + content: "let x = 1" + }; + const f3 = { + path: "/c/app.ts", + content: "let x = 1" + }; + const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); + const p1 = makeProject(f1); + const p2 = makeProject(f2); + const p3 = makeProject(f3); + + const host = createServerHost([f1, f2, f3]); + const session = createSession(host); + + session.executeCommand({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p2] } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); + + session.executeCommand({ + seq: 2, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p3] } + }); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [] } + }); + checkNumberOfProjects(projectService, { externalProjects: 0 }); + + session.executeCommand({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p2] } + }); + assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); + }); + + it("should not close external project with no open files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, file2.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + // close client file - external project should still exists + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); + + it("external project for dynamic file", () => { + const externalProjectName = "^ScriptDocument1 file1.ts"; + const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]); + const host = createServerHost([]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: externalFiles, + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + + externalFiles[0].content = "let x =1;"; + projectService.applyChangesInOpenFiles(externalFiles, [], []); + }); + + it("external project that included config files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + const file2 = { + path: "/a/c/f2.ts", + content: "let y =1;" + }; + const config2 = { + path: "/a/c/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f2.ts"] + } + ) + }; + const file3 = { + path: "/a/d/f3.ts", + content: "let z =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2, file3, config1, config2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + const proj1 = projectService.configuredProjects.get(config1.path); + const proj2 = projectService.configuredProjects.get(config2.path); + assert.isDefined(proj1); + assert.isDefined(proj2); + + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); + + projectService.openClientFile(file3.path, file3.content); + checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); + + projectService.closeExternalProject(externalProjectName); + // open file 'file1' from configured project keeps project alive + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + + projectService.closeClientFile(file3.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + + projectService.openClientFile(file2.path, file2.content); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config1.path)); + assert.isDefined(projectService.configuredProjects.get(config2.path)); + }); + + it("external project with included config file opened after configured project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.openExternalProject({ + rootFiles: toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.closeClientFile(file1.path); + // configured project is alive since it is opened as part of external project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + projectService.closeExternalProject(externalProjectName); + checkNumberOfProjects(projectService, { configuredProjects: 0 }); + }); + + it("external project with included config file opened after configured project and then closed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/f2.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2, libFile, configFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); + + projectService.openExternalProject({ + rootFiles: toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.closeExternalProject(externalProjectName); + // configured project is alive since file is still open + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(configFile.path)); + }); + + it("can correctly update external project when set of root files has changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + }); + + it("can update external project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "m"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let y = 1" + }; + const file3 = { + path: "/a/m.ts", + content: "export let y = 1" + }; + + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); + + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); + }); + + it("language service disabled state is updated in external projects", () => { + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const host = createServerHost([f1, f2]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const service = createProjectService(host); + const projectFileName = "/a/proj.csproj"; + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); + }); + + describe("deleting config file opened from the external project works", () => { + function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) { + const site = { + path: "/user/someuser/project/js/site.js", + content: "" + }; + const configFile = { + path: "/user/someuser/project/tsconfig.json", + content: "{}" + }; + const projectFileName = "/user/someuser/project/WebApplication6.csproj"; + const host = createServerHost([libFile, site, configFile]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + + const externalProject: protocol.ExternalProject = { + projectFileName, + rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)], + options: { allowJs: false }, + typeAcquisition: { include: [] } + }; + + projectService.openExternalProjects([externalProject]); + + let knownProjects = projectService.synchronizeProjectList([]); + checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); + + const configProject = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? + emptyArray : // Since no files opened from this project, its not loaded + [configFile.path]); + + host.reloadFS([libFile, site]); + host.checkTimeoutQueueLengthAndRun(1); + + knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 + checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); + + externalProject.rootFiles.length = 1; + projectService.openExternalProjects([externalProject]); + + checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); + checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + describe("correctly handling add/remove tsconfig - 1", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const f2 = { + path: "/a/b/lib.ts", + content: "" + }; + const tsconfig = { + path: "/a/b/tsconfig.json", + content: "" + }; + const host = createServerHost([f1, f2]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + + // rename lib.ts to tsconfig.json + host.reloadFS([f1, tsconfig]); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, tsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); + + // rename tsconfig.json back to lib.ts + host.reloadFS([f1, f2]); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + describe("correctly handling add/remove tsconfig - 2", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const cLib = { + path: "/a/b/c/lib.ts", + content: "" + }; + const cTsconfig = { + path: "/a/b/c/tsconfig.json", + content: "{}" + }; + const dLib = { + path: "/a/b/d/lib.ts", + content: "" + }; + const dTsconfig = { + path: "/a/b/d/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); + + // remove one config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, dTsconfig.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); + + // remove second config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + + // open two config files + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); + + // close all projects - no projects should be opened + projectService.closeExternalProject(projectName); + projectService.checkNumberOfProjects({}); + } + + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); + }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + + it("correctly handles changes in lib section of config file", () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5" + ] + } + }) + }; + const config2 = { + path: config1.path, + content: JSON.stringify( + { + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5", + "es2015.promise" + ] + } + }) + }; + const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const projectService = createProjectService(host); + projectService.openClientFile(app.path); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); + + host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(2); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); + }); + + it("should handle non-existing directories in config file", () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + const host = createServerHost([f, config]); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + assert.isTrue(project.hasOpenRef()); // f + + projectService.closeClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.isTrue(project.hasOpenRef()); // f + assert.isFalse(project.isClosed()); + }); + + it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; + const projectFileName = "/a/b/project.csproj"; + const host = createServerHost([f1, config]); + const service = createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, config.path]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + checkProjectActualFiles(project, emptyArray); + + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project, [config.path, f1.path]); + + service.closeExternalProject(projectFileName); + service.checkNumberOfProjects({}); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, config.path]), + options: {} + }); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project2 = service.configuredProjects.get(config.path)!; + assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project2, [config.path, f1.path]); + }); + + it("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => { + const projectLocation = `/user/username/projects/WebApplication36/WebApplication36`; + const projectFileName = `${projectLocation}/WebApplication36.csproj`; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: "{}" + }; + const files = [libFile, tsconfig]; + const host = createServerHost(files); + const service = createProjectService(host); + + // Create external project + service.openExternalProjects([{ + projectFileName, + rootFiles: [{ fileName: tsconfig.path }], + options: { allowJs: false } + }]); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!; + checkProjectActualFiles(configProject, [tsconfig.path]); + + // write js file, open external project and open it for edit + const jsFilePath = `${projectLocation}/javascript.js`; + host.writeFile(jsFilePath, ""); + service.openExternalProjects([{ + projectFileName, + rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }], + options: { allowJs: false } + }]); + service.applyChangesInOpenFiles([ + { fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" } + ], /*changedFiles*/ undefined, /*closedFiles*/ undefined); + checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + checkProjectActualFiles(configProject, [tsconfig.path]); + const inferredProject = service.inferredProjects[0]; + checkProjectActualFiles(inferredProject, [libFile.path, jsFilePath]); + + // write jsconfig file + const jsConfig: File = { + path: `${projectLocation}/jsconfig.json`, + content: "{}" + }; + // Dont invoke file creation watchers as the repro suggests + host.ensureFileOrFolder(jsConfig, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); + + // Open external project + service.openExternalProjects([{ + projectFileName, + rootFiles: [{ fileName: jsConfig.path }, { fileName: tsconfig.path }, { fileName: jsFilePath }], + options: { allowJs: false } + }]); + checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); + checkProjectActualFiles(configProject, [tsconfig.path]); + assert.isTrue(inferredProject.isOrphan()); + const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!; + checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, libFile.path]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts new file mode 100644 index 00000000000..bb1be544a2c --- /dev/null +++ b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts @@ -0,0 +1,45 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => { + it("works when extends is specified with a case insensitive file system", () => { + const rootPath = "/Users/username/dev/project"; + const file1: File = { + path: `${rootPath}/index.ts`, + content: 'import {x} from "file2";', + }; + const file2: File = { + path: `${rootPath}/file2.js`, + content: "", + }; + const file2Dts: File = { + path: `${rootPath}/types/file2/index.d.ts`, + content: "export declare const x: string;", + }; + const tsconfigAll: File = { + path: `${rootPath}/tsconfig.all.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { file2: ["./file2.js"] }, + typeRoots: ["./types"], + forceConsistentCasingInFileNames: true, + }, + }), + }; + const tsconfig: File = { + path: `${rootPath}/tsconfig.json`, + content: JSON.stringify({ extends: "./tsconfig.all.json" }), + }; + + const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); + const session = createSession(host); + + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/formatSettings.ts b/src/testRunner/unittests/tsserver/formatSettings.ts new file mode 100644 index 00000000000..2d09ed8a8a0 --- /dev/null +++ b/src/testRunner/unittests/tsserver/formatSettings.ts @@ -0,0 +1,39 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: format settings", () => { + it("can be set globally", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x;" + }; + const host = createServerHost([f1]); + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + + const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); + + // set global settings + const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks }; + projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 }); + + // get format options for file - should be equal to new global settings + const s1 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); + assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings"); + + // set per file format options + const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter }; + projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path }); + + // get format options for file - should be equal to new per-file settings + const s2 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); + assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings"); + + // set new global settings - they should not affect ones that were set per-file + const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements }; + projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 }); + + // get format options for file - should be equal to new per-file settings + const s3 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); + assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings"); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/getApplicableRefactors.ts b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts new file mode 100644 index 00000000000..1f9576e1d63 --- /dev/null +++ b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts @@ -0,0 +1,12 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: getApplicableRefactors", () => { + it("works when taking position", () => { + const aTs: File = { path: "/a.ts", content: "" }; + const session = createSession(createServerHost([aTs])); + openFilesForSession([aTs], session); + const response = executeSessionRequest( + session, protocol.CommandTypes.GetApplicableRefactors, { file: aTs.path, line: 1, offset: 1 }); + assert.deepEqual | undefined>(response, []); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts new file mode 100644 index 00000000000..2b28de7b121 --- /dev/null +++ b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts @@ -0,0 +1,105 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: getEditsForFileRename", () => { + it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { + const userTs: File = { + path: "/user.ts", + content: 'import { x } from "./old";', + }; + const newTs: File = { + path: "/new.ts", + content: "export const x = 0;", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([userTs, newTs, tsconfig]); + const projectService = createProjectService(host); + projectService.openClientFile(userTs.path); + const project = projectService.configuredProjects.get(tsconfig.path)!; + + Debug.assert(!!project.resolveModuleNames); + + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions); + assert.deepEqual>(edits, [{ + fileName: "/user.ts", + textChanges: [{ + span: textSpanFromSubstring(userTs.content, "./old"), + newText: "./new", + }], + }]); + }); + + it("works with multiple projects", () => { + const aUserTs: File = { + path: "/a/user.ts", + content: 'import { x } from "./old";', + }; + const aOldTs: File = { + path: "/a/old.ts", + content: "export const x = 0;", + }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), + }; + const bUserTs: File = { + path: "/b/user.ts", + content: 'import { x } from "../a/old";', + }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: "{}", + }; + + const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aUserTs, bUserTs], session); + + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aOldTs.path, + newFilePath: "/a/new.ts", + }); + assert.deepEqual>(response, [ + { + fileName: aTsconfig.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], + }, + { + fileName: aUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], + }, + { + fileName: bUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], + }, + ]); + }); + + it("works with file moved to inferred project", () => { + const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' }; + const cTs: File = { path: "/c.ts", content: "export {};" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; + + const host = createServerHost([aTs, cTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs, cTs], session); + + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: "/b.ts", + newFilePath: cTs.path, + }); + assert.deepEqual>(response, [ + { + fileName: "/tsconfig.json", + textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], + }, + { + fileName: "/a.ts", + textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], + }, + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts new file mode 100644 index 00000000000..3e79451cd11 --- /dev/null +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -0,0 +1,665 @@ +namespace ts.projectSystem { + export import TI = server.typingsInstaller; + export import protocol = server.protocol; + export import CommandNames = server.CommandNames; + + export import TestServerHost = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export type Folder = TestFSWithWatch.Folder; + export import createServerHost = TestFSWithWatch.createServerHost; + export import checkArray = TestFSWithWatch.checkArray; + export import libFile = TestFSWithWatch.libFile; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + + export import commonFile1 = tscWatch.commonFile1; + export import commonFile2 = tscWatch.commonFile2; + + const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; + export function mapOutputToJson(s: string) { + return convertToObject( + parseJsonText("json.json", s.replace(outputEventRegex, "")), + [] + ); + } + + export const customTypesMap = { + path: "/typesMap.json", + content: `{ + "typesMap": { + "jquery": { + "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", + "types": ["jquery"] + }, + "quack": { + "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", + "types": ["duck-types"] + } + }, + "simpleMap": { + "Bacon": "baconjs", + "bliss": "blissfuljs", + "commander": "commander", + "cordova": "cordova", + "react": "react", + "lodash": "lodash" + } + }` + }; + + export interface PostExecAction { + readonly success: boolean; + readonly callback: TI.RequestCompletedAction; + } + + export const nullLogger: server.Logger = { + close: noop, + hasLevel: returnFalse, + loggingEnabled: returnFalse, + perftrc: noop, + info: noop, + msg: noop, + startGroup: noop, + endGroup: noop, + getLogFileName: () => undefined, + }; + + export function createHasErrorMessageLogger() { + let hasErrorMsg = false; + const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; + const logger: server.Logger = { + close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, + msg: (s, type) => { + Debug.fail(`Error: ${s}, type: ${type}`); + hasErrorMsg = true; + } + }; + return { logger, hasErrorMsg: () => hasErrorMsg }; + } + + export function createLoggerWritingToConsole(): server.Logger { + const { close, startGroup, endGroup, getLogFileName } = nullLogger; + return { + close, + hasLevel: returnTrue, + loggingEnabled: returnTrue, + perftrc: s => console.log(s), + info: s => console.log(s), + msg: (s, type) => console.log(`${type}:: ${s}`), + startGroup, + endGroup, + getLogFileName + }; + } + + export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { + protected projectService!: server.ProjectService; + constructor( + readonly globalTypingsCacheLocation: string, + throttleLimit: number, + installTypingHost: server.ServerHost, + readonly typesRegistry = createMap>(), + log?: TI.Log) { + super(installTypingHost, globalTypingsCacheLocation, TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); + } + + protected postExecActions: PostExecAction[] = []; + + isKnownTypesPackageName = notImplemented; + installPackage = notImplemented; + inspectValue = notImplemented; + + executePendingCommands() { + const actionsToRun = this.postExecActions; + this.postExecActions = []; + for (const action of actionsToRun) { + action.callback(action.success); + } + } + + checkPendingCommands(expectedCount: number) { + assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); + } + + onProjectClosed = noop; + + attach(projectService: server.ProjectService) { + this.projectService = projectService; + } + + getInstallTypingHost() { + return this.installTypingHost; + } + + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + this.addPostExecAction("success", cb); + } + + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { + this.projectService.updateTypingsForProject(response); + } + + enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { + const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); + this.install(request); + } + + addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { + const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); + const action: PostExecAction = { + success: !!out, + callback: cb + }; + this.postExecActions.push(action); + } + } + + function createNpmPackageJsonString(installedTypings: string[]): string { + const dependencies: MapLike = {}; + for (const typing of installedTypings) { + dependencies[typing] = "1.0.0"; + } + return JSON.stringify({ dependencies }); + } + + export function createTypesRegistry(...list: string[]): Map> { + const versionMap = { + "latest": "1.3.0", + "ts2.0": "1.0.0", + "ts2.1": "1.0.0", + "ts2.2": "1.2.0", + "ts2.3": "1.3.0", + "ts2.4": "1.3.0", + "ts2.5": "1.3.0", + "ts2.6": "1.3.0", + "ts2.7": "1.3.0" + }; + const map = createMap>(); + for (const l of list) { + map.set(l, versionMap); + } + return map; + } + + export function toExternalFile(fileName: string): protocol.ExternalFile { + return { fileName }; + } + + export function toExternalFiles(fileNames: string[]) { + return map(fileNames, toExternalFile); + } + + export function fileStats(nonZeroStats: Partial): server.FileStats { + return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; + } + + export interface ConfigFileDiagnostic { + fileName: string | undefined; + start: number | undefined; + length: number | undefined; + messageText: string; + category: DiagnosticCategory; + code: number; + reportsUnnecessary?: {}; + source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; + } + + export class TestServerEventManager { + private events: server.ProjectServiceEvent[] = []; + readonly session: TestSession; + readonly service: server.ProjectService; + readonly host: TestServerHost; + constructor(files: File[], suppressDiagnosticEvents?: boolean) { + this.host = createServerHost(files); + this.session = createSession(this.host, { + canUseEvents: true, + eventHandler: event => this.events.push(event), + suppressDiagnosticEvents, + }); + this.service = this.session.getProjectService(); + } + + getEvents(): ReadonlyArray { + const events = this.events; + this.events = []; + return events; + } + + getEvent(eventName: T["eventName"]): T["data"] { + let eventData: T["data"] | undefined; + filterMutate(this.events, e => { + if (e.eventName === eventName) { + if (eventData !== undefined) { + assert(false, "more than one event found"); + } + eventData = e.data; + return false; + } + return true; + }); + return Debug.assertDefined(eventData); + } + + hasZeroEvent(eventName: T["eventName"]) { + this.events.forEach(event => assert.notEqual(event.eventName, eventName)); + } + + checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: ReadonlyArray) { + const eventData = this.getEvent(server.ConfigFileDiagEvent); + assert.equal(eventData.configFileName, configFileName); + assert.equal(eventData.triggerFile, triggerFile); + const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); + if (errors) { + assert.deepEqual(actual, errors); + } + } + + assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { + assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { + projectId: sys.createSHA256Hash!(configFile), + fileStats: fileStats({ ts: 1 }), + compilerOptions: {}, + extends: false, + files: false, + include: false, + exclude: false, + compileOnSave: false, + typeAcquisition: { + enable: false, + exclude: false, + include: false, + }, + configFileName: "tsconfig.json", + projectType: "configured", + languageServiceEnabled: true, + version, + ...partial, + }); + } + + assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { + assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); + } + assertNoOpenFilesTelemetryEvent(): void { + this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); + } + } + + export class TestSession extends server.Session { + private seq = 0; + public events: protocol.Event[] = []; + public host!: TestServerHost; + + getProjectService() { + return this.projectService; + } + + public getSeq() { + return this.seq; + } + + public getNextSeq() { + return this.seq + 1; + } + + public executeCommandSeq(request: Partial) { + this.seq++; + request.seq = this.seq; + request.type = "request"; + return this.executeCommand(request); + } + + public event(body: T, eventName: string) { + this.events.push(server.toEvent(eventName, body)); + super.event(body, eventName); + } + + public clearMessages() { + clear(this.events); + this.host.clearOutput(); + } + } + + export function createSession(host: server.ServerHost, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); + } + + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } + + const sessionOptions: server.SessionOptions = { + host, + cancellationToken: server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: opts.logger || createHasErrorMessageLogger().logger, + canUseEvents: false + }; + + return new TestSession({ ...sessionOptions, ...opts }); + } + + export function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { + const events: T[] = []; + const session = createSession(host, { + eventHandler: e => { + if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { + events.push(e as T); + } + } + }); + + return { session, events }; + } + + export function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { + const session = createSession(host, { canUseEvents: true, ...opts }); + + return { + session, + getEvents, + clearEvents + }; + + function getEvents() { + return mapDefined(host.getOutput(), s => { + const e = mapOutputToJson(s); + return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; + }); + } + + function clearEvents() { + session.clearMessages(); + } + } + + export interface CreateProjectServiceParameters { + cancellationToken?: HostCancellationToken; + logger?: server.Logger; + useSingleInferredProject?: boolean; + typingsInstaller?: server.ITypingsInstaller; + eventHandler?: server.ProjectServiceEventHandler; + } + + export class TestProjectService extends server.ProjectService { + constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, + typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { + super({ + host, + logger, + cancellationToken, + useSingleInferredProject, + useInferredProjectPerProjectRoot: false, + typingsInstaller, + typesMapLocation: customTypesMap.path, + eventHandler, + ...opts + }); + } + + checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfProjects(this, count); + } + } + export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { + const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; + const logger = parameters.logger || createHasErrorMessageLogger().logger; + const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; + return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 + } + + export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); + } + + export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); + } + + export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { + assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); + } + + export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { + checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); + checkNumberOfExternalProjects(projectService, count.externalProjects || 0); + checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); + } + + export function configuredProjectAt(projectService: server.ProjectService, index: number) { + const values = projectService.configuredProjects.values(); + while (index > 0) { + values.next(); + index--; + } + return values.next().value; + } + + export function checkProjectActualFiles(project: server.Project, expectedFiles: ReadonlyArray) { + checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); + } + + export function checkProjectRootFiles(project: server.Project, expectedFiles: ReadonlyArray) { + checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + } + + export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { + dir = normalizePath(dir); + const result: string[] = []; + forEachAncestorDirectory(dir, ancestor => { + if (mapAncestor(ancestor)) { + result.push(combinePaths(ancestor, path2)); + } + }); + return result; + } + + export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { + return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); + } + + export const nodeModules = "node_modules"; + export function getNodeModuleDirectories(dir: string) { + return getRootsToWatchWithAncestorDirectory(dir, nodeModules); + } + + export const nodeModulesAtTypes = "node_modules/@types"; + export function getTypeRootsFromLocation(currentDirectory: string) { + return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); + } + + export function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { + checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); + } + + export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: ReadonlyArray) { + checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); + } + + export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location { + const start = str.indexOf(substring); + Debug.assert(start !== -1); + return protocolToLocation(str)(start); + } + + function protocolToLocation(text: string): (pos: number) => protocol.Location { + const lineStarts = computeLineStarts(text); + return pos => { + const x = computeLineAndCharacterOfPosition(lineStarts, pos); + return { line: x.line + 1, offset: x.character + 1 }; + }; + } + + export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { + const span = textSpanFromSubstring(str, substring, options); + const toLocation = protocolToLocation(str); + return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; + } + + export function protocolRenameSpanFromSubstring( + str: string, + substring: string, + options?: SpanFromSubstringOptions, + prefixSuffixText?: { readonly prefixText?: string, readonly suffixText?: string }, + ): protocol.RenameTextSpan { + return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText }; + } + + export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { + const start = nthIndexOf(str, substring, options ? options.index : 0); + Debug.assert(start !== -1); + return createTextSpan(start, substring.length); + } + + export function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs { + return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) }; + } + + export interface SpanFromSubstringOptions { + readonly index: number; + } + + function nthIndexOf(str: string, substr: string, n: number): number { + let index = -1; + for (; n >= 0; n--) { + index = str.indexOf(substr, index + 1); + if (index === -1) return -1; + } + return index; + } + + /** + * Test server cancellation token used to mock host token cancellation requests. + * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls + * should be made before canceling the token. The id of the request to cancel should be set with + * setRequestToCancel(); + */ + export class TestServerCancellationToken implements server.ServerCancellationToken { + private currentId: number | undefined = -1; + private requestToCancel = -1; + private isCancellationRequestedCount = 0; + + constructor(private cancelAfterRequest = 0) { + } + + setRequest(requestId: number) { + this.currentId = requestId; + } + + setRequestToCancel(requestId: number) { + this.resetToken(); + this.requestToCancel = requestId; + } + + resetRequest(requestId: number) { + assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); + this.currentId = undefined; + } + + isCancellationRequested() { + this.isCancellationRequestedCount++; + // If the request id is the request to cancel and isCancellationRequestedCount + // has been met then cancel the request. Ex: cancel the request if it is a + // nav bar request & isCancellationRequested() has already been called three times. + return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; + } + + resetToken() { + this.currentId = -1; + this.isCancellationRequestedCount = 0; + this.requestToCancel = -1; + } + } + + export function makeSessionRequest(command: string, args: T): protocol.Request { + return { + seq: 0, + type: "request", + command, + arguments: args + }; + } + + export function executeSessionRequest(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { + return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; + } + + export function executeSessionRequestNoResponse(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { + session.executeCommand(makeSessionRequest(command, args)); + } + + export function openFilesForSession(files: ReadonlyArray, session: server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Open, + "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); + } + } + + export function closeFilesForSession(files: ReadonlyArray, session: server.Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); + } + } + + export interface ErrorInformation { + diagnosticMessage: DiagnosticMessage; + errorTextArguments?: string[]; + } + + function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { + return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); + } + + export function verifyDiagnostics(actual: ReadonlyArray, expected: ReadonlyArray) { + const expectedErrors = expected.map(getProtocolDiagnosticMessage); + assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); + } + + export function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { + verifyDiagnostics(actual, []); + } + + export function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { + checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); + } + + export function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { + return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; + } + + export function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { + checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); + } + + export function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); + } + + export function checkNoDiagnosticEvents(session: TestSession) { + for (const event of session.events) { + assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); + } + } + + export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + const events = session.events; + assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + + const outputs = session.host.getOutput(); + assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); + + if (isMostRecent) { + assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); + } + } +} diff --git a/src/testRunner/unittests/tsserver/importHelpers.ts b/src/testRunner/unittests/tsserver/importHelpers.ts new file mode 100644 index 00000000000..b1ec5955eaa --- /dev/null +++ b/src/testRunner/unittests/tsserver/importHelpers.ts @@ -0,0 +1,18 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: import helpers", () => { + it("should not crash in tsserver", () => { + const f1 = { + path: "/a/app.ts", + content: "export async function foo() { return 100; }" + }; + const tslib = { + path: "/a/node_modules/tslib/index.d.ts", + content: "" + }; + const host = createServerHost([f1, tslib]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } }); + service.checkNumberOfProjects({ externalProjects: 1 }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts new file mode 100644 index 00000000000..9d7ccaa1224 --- /dev/null +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -0,0 +1,349 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: Inferred projects", () => { + it("create inferred project", () => { + const appFile: File = { + path: "/a/b/c/app.ts", + content: ` + import {f} from "./module" + console.log(f) + ` + }; + + const moduleFile: File = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + const host = createServerHost([appFile, moduleFile, libFile]); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(appFile.path); + + assert(!configFileName, `should not find config, got: '${configFileName}`); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + + const project = projectService.inferredProjects[0]; + + checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); + const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false); + checkWatchedDirectories(host, [combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true); + }); + + it("should use only one inferred project if 'useOneInferredProject' is set", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts" ] + }` + }; + const file2 = { + path: "/a/c/main.ts", + content: "let x =1;" + }; + + const file3 = { + path: "/a/d/main.ts", + content: "let x =1;" + }; + + const host = createServerHost([file1, file2, file3, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); + + + host.reloadFS([file1, configFile, file2, file3, libFile]); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); + }); + + it("disable inferred project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + + const host = createServerHost([file1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + + projectService.openClientFile(file1.path, file1.content); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + + assert.isFalse(proj.languageServiceEnabled); + }); + + it("project settings for inferred projects", () => { + const file1 = { + path: "/a/b/app.ts", + content: `import {x} from "mod"` + }; + const modFile = { + path: "/a/mod.ts", + content: "export let x: number" + }; + const host = createServerHost([file1, modFile]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(modFile.path); + + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + const inferredProjects = projectService.inferredProjects.slice(); + checkProjectActualFiles(inferredProjects[0], [file1.path]); + checkProjectActualFiles(inferredProjects[1], [modFile.path]); + + projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic }); + host.checkTimeoutQueueLengthAndRun(3); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]); + assert.isTrue(inferredProjects[1].isOrphan()); + }); + + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createServerHost([f]); + const session = createSession(host); + session.executeCommand({ + seq: 1, + type: "request", + command: "compilerOptionsForInferredProjects", + arguments: { + options: { + allowJs: true + } + } + }); + session.executeCommand({ + seq: 2, + type: "request", + command: "open", + arguments: { + file: f.path, + fileContent: f.content, + scriptKindName: "JS" + } + }); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); + }); + + it("inferred projects per project root", () => { + const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; + const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; + const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; + const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; + const host = createServerHost([file1, file2, file3, file4]); + const session = createSession(host, { + useSingleInferredProject: true, + useInferredProjectPerProjectRoot: true + }); + session.executeCommand({ + seq: 1, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ESNext + } + } + }); + session.executeCommand({ + seq: 2, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ES2015 + }, + projectRootPath: "/b" + } + }); + session.executeCommand({ + seq: 3, + type: "request", + command: CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "JS", + projectRootPath: file1.projectRootPath + } + }); + session.executeCommand({ + seq: 4, + type: "request", + command: CommandNames.Open, + arguments: { + file: file2.path, + fileContent: file2.content, + scriptKindName: "JS", + projectRootPath: file2.projectRootPath + } + }); + session.executeCommand({ + seq: 5, + type: "request", + command: CommandNames.Open, + arguments: { + file: file3.path, + fileContent: file3.content, + scriptKindName: "JS", + projectRootPath: file3.projectRootPath + } + }); + session.executeCommand({ + seq: 6, + type: "request", + command: CommandNames.Open, + arguments: { + file: file4.path, + fileContent: file4.content, + scriptKindName: "JS" + } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); + }); + + function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) { + checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); + assert.equal(inferredProject.getCompilationSettings().target, target); + } + + function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const files: [File, File, File, File] = [ + { path: "/a/file1.ts", content: "let x = 1;" }, + { path: "/A/file2.ts", content: "let y = 2;" }, + { path: "/b/file2.ts", content: "let x = 3;" }, + { path: "/c/file3.ts", content: "let z = 4;" } + ]; + const host = createServerHost(files, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host, { useSingleInferredProject: true, }, { useInferredProjectPerProjectRoot: true }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ESNext + }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ES2015 + }, "/a"); + + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2015], + [[files[2]], ScriptTarget.ESNext] + ]); + closeClientFiles(); + + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0]], ScriptTarget.ES2015], + [[files[1]], ScriptTarget.ESNext], + [[files[2]], ScriptTarget.ESNext] + ]); + } + else { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2015], + [[files[2]], ScriptTarget.ESNext] + ]); + } + closeClientFiles(); + + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ES2017 + }, "/A"); + + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + closeClientFiles(); + + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0]], ScriptTarget.ES2015], + [[files[1]], ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + } + else { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + } + closeClientFiles(); + + function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) { + files.forEach((file, index) => { + projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]); + }); + } + + function closeClientFiles() { + files.forEach(file => projectService.closeClientFile(file.path)); + } + + function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) { + checkNumberOfProjects(projectService, { inferredProjects: expected.length }); + projectService.inferredProjects.forEach((p, index) => { + const [actualFiles, target] = expected[index]; + checkInferredProject(p, actualFiles, target); + }); + } + } + + it("inferred projects per project root with case sensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + + it("inferred projects per project root with case insensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/languageService.ts b/src/testRunner/unittests/tsserver/languageService.ts new file mode 100644 index 00000000000..86d426664df --- /dev/null +++ b/src/testRunner/unittests/tsserver/languageService.ts @@ -0,0 +1,19 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: Language service", () => { + it("should work correctly on case-sensitive file systems", () => { + const lib = { + path: "/a/Lib/lib.d.ts", + content: "let x: number" + }; + const f = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + projectService.inferredProjects[0].getLanguageService().getProgram(); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts new file mode 100644 index 00000000000..eb254789d67 --- /dev/null +++ b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts @@ -0,0 +1,55 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: maxNodeModuleJsDepth for inferred projects", () => { + it("should be set to 2 if the project has js root files", () => { + const file1: File = { + path: "/a/b/file1.js", + content: `var t = require("test"); t.` + }; + const moduleFile: File = { + path: "/a/b/node_modules/test/index.js", + content: `var v = 10; module.exports = v;` + }; + + const host = createServerHost([file1, moduleFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + + let project = projectService.inferredProjects[0]; + let options = project.getCompilationSettings(); + assert.isTrue(options.maxNodeModuleJsDepth === 2); + + // Assert the option sticks + projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 }); + project = projectService.inferredProjects[0]; + options = project.getCompilationSettings(); + assert.isTrue(options.maxNodeModuleJsDepth === 2); + }); + + it("should return to normal state when all js root files are removed from project", () => { + const file1 = { + path: "/a/file1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/file2.js", + content: "let x =1;" + }; + + const host = createServerHost([file1, file2, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + + projectService.openClientFile(file1.path); + checkNumberOfInferredProjects(projectService, 1); + let project = projectService.inferredProjects[0]; + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); + + projectService.openClientFile(file2.path); + project = projectService.inferredProjects[0]; + assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2); + + projectService.closeClientFile(file2.path); + project = projectService.inferredProjects[0]; + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/metadataInResponse.ts b/src/testRunner/unittests/tsserver/metadataInResponse.ts new file mode 100644 index 00000000000..6ec8a565d87 --- /dev/null +++ b/src/testRunner/unittests/tsserver/metadataInResponse.ts @@ -0,0 +1,99 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: with metadata in response", () => { + const metadata = "Extra Info"; + function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { + const output = host.getOutput().map(mapOutputToJson); + assert.deepEqual(output, [expectedResponse]); + host.clearOutput(); + } + + function verifyCommandWithMetadata(session: TestSession, host: TestServerHost, command: Partial, expectedResponseBody: U) { + command.seq = session.getSeq(); + command.type = "request"; + session.onMessage(JSON.stringify(command)); + verifyOutput(host, expectedResponseBody ? + { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } : + { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." } + ); + } + + const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { plugins: [{ name: "myplugin" }] } + }) + }; + function createHostWithPlugin(files: ReadonlyArray) { + const host = createServerHost(files); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: server.PluginCreateInfo) { + const proxy = Harness.LanguageService.makeDefaultProxy(info); + proxy.getCompletionsAtPosition = (filename, position, options) => { + const result = info.languageService.getCompletionsAtPosition(filename, position, options); + if (result) { + result.metadata = metadata; + } + return result; + }; + return proxy; + } + }), + error: undefined + }; + }; + return host; + } + + describe("With completion requests", () => { + const completionRequestArgs: protocol.CompletionsRequestArgs = { + file: aTs.path, + line: 1, + offset: aTs.content.indexOf("this.") + 1 + "this.".length + }; + const expectedCompletionEntries: ReadonlyArray = [ + { name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" }, + { name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" } + ]; + + it("can pass through metadata when the command returns array", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata>(session, host, { + command: protocol.CommandTypes.Completions, + arguments: completionRequestArgs + }, expectedCompletionEntries); + }); + + it("can pass through metadata when the command returns object", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.CompletionInfo, + arguments: completionRequestArgs + }, { + isGlobalCompletion: false, + isMemberCompletion: true, + isNewIdentifierLocation: false, + entries: expectedCompletionEntries + }); + }); + + it("returns undefined correctly", () => { + const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.Completions, + arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } + }, /*expectedResponseBody*/ undefined); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/navTo.ts b/src/testRunner/unittests/tsserver/navTo.ts new file mode 100644 index 00000000000..b3aebef1043 --- /dev/null +++ b/src/testRunner/unittests/tsserver/navTo.ts @@ -0,0 +1,30 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: navigate-to for javascript project", () => { + function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { + return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; + } + + it("should not include type symbols", () => { + const file1: File = { + path: "/a/b/file1.js", + content: "function foo() {}" + }; + const configFile: File = { + path: "/a/b/jsconfig.json", + content: "{}" + }; + const host = createServerHost([file1, configFile, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + + // Try to find some interface type defined in lib.d.ts + const libTypeNavToRequest = makeSessionRequest(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); + const items = session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[]; + assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); + + const localFunctionNavToRequst = makeSessionRequest(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); + const items2 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]; + assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/occurences.ts b/src/testRunner/unittests/tsserver/occurences.ts new file mode 100644 index 00000000000..cf9c83e2e72 --- /dev/null +++ b/src/testRunner/unittests/tsserver/occurences.ts @@ -0,0 +1,47 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: occurence highlight on string", () => { + it("should be marked if only on string values", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` + }; + + const host = createServerHost([file1]); + const session = createSession(host); + const projectService = session.getProjectService(); + + projectService.openClientFile(file1.path); + { + const highlightRequest = makeSessionRequest( + CommandNames.Occurrences, + { file: file1.path, line: 1, offset: 11 } + ); + const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; + const firstOccurence = highlightResponse[0]; + assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); + } + + { + const highlightRequest = makeSessionRequest( + CommandNames.Occurrences, + { file: file1.path, line: 3, offset: 13 } + ); + const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; + assert.isTrue(highlightResponse.length === 2); + const firstOccurence = highlightResponse[0]; + assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name"); + } + + { + const highlightRequest = makeSessionRequest( + CommandNames.Occurrences, + { file: file1.path, line: 4, offset: 14 } + ); + const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; + assert.isTrue(highlightResponse.length === 2); + const firstOccurence = highlightResponse[0]; + assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer"); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/openFile.ts b/src/testRunner/unittests/tsserver/openFile.ts new file mode 100644 index 00000000000..d2a995c68f8 --- /dev/null +++ b/src/testRunner/unittests/tsserver/openFile.ts @@ -0,0 +1,108 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: Open-file", () => { + it("can be reloaded with empty content", () => { + const f = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const projectFileName = "externalProject"; + const host = createServerHost([f]); + const projectService = createProjectService(host); + // create a project + projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + + const p = projectService.externalProjects[0]; + // force to load the content of the file + p.updateGraph(); + + const scriptInfo = p.getScriptInfo(f.path)!; + checkSnapLength(scriptInfo.getSnapshot(), f.content.length); + + // open project and replace its content with empty string + projectService.openClientFile(f.path, ""); + checkSnapLength(scriptInfo.getSnapshot(), 0); + }); + function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) { + assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); + } + + function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { + const file1: File = { + path: "/a/b/src/app.ts", + content: "let x = 10;" + }; + const file2: File = { + path: "/a/B/lib/module2.ts", + content: "let z = 10;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "" + }; + const configFile2: File = { + path: "/a/tsconfig.json", + content: "" + }; + const host = createServerHost([file1, file2, configFile, configFile2], { + useCaseSensitiveFileNames + }); + const service = createProjectService(host); + + // Open file1 -> configFile + verifyConfigFileName(file1, "/a", configFile); + verifyConfigFileName(file1, "/a/b", configFile); + verifyConfigFileName(file1, "/a/B", configFile); + + // Open file2 use root "/a/b" + verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); + verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); + verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); + + function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) { + const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); + assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); + service.closeClientFile(file.path); + } + } + it("works when project root is used with case-sensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); + }); + + it("works when project root is used with case-insensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); + }); + + it("uses existing project even if project refresh is pending", () => { + const projectFolder = "/user/someuser/projects/myproject"; + const aFile: File = { + path: `${projectFolder}/src/a.ts`, + content: "export const x = 0;" + }; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, configFile, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); + verifyProject(); + + const bFile: File = { + path: `${projectFolder}/src/b.ts`, + content: `export {}; declare module "./a" { export const y: number; }` + }; + files.push(bFile); + host.reloadFS(files); + service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); + verifyProject(); + + function verifyProject() { + assert.isDefined(service.configuredProjects.get(configFile.path)); + const project = service.configuredProjects.get(configFile.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + } + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts new file mode 100644 index 00000000000..9e24771f3ae --- /dev/null +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -0,0 +1,849 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: Project Errors", () => { + function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: ReadonlyArray): void { + assert.isTrue(projectFiles !== undefined, "missing project files"); + checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); + } + + function checkProjectErrorsWorker(errors: ReadonlyArray, expectedErrors: ReadonlyArray): void { + assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); + if (expectedErrors.length) { + for (let i = 0; i < errors.length; i++) { + const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n"); + const expectedMessage = expectedErrors[i]; + assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); + } + } + } + + function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { + assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); + if (expectedErrors.length) { + zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { + assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); + }); + } + } + + it("external project - diagnostics for missing files", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/applib.ts", + content: "" + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/test.csproj"; + const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }; + + { + projectService.openExternalProject({ + projectFileName, + options: {}, + rootFiles: toExternalFiles([file1.path, file2.path]) + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + // only file1 exists - expect error + checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); + } + host.reloadFS([file2, libFile]); + { + // only file2 exists - expect error + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); + } + + host.reloadFS([file1, file2, libFile]); + { + // both files exist - expect no errors + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, []); + } + }); + + it("configured projects - diagnostics for missing files", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/applib.ts", + content: "" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) + }; + const host = createServerHost([file1, config, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + openFilesForSession([file1], session); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: project.getProjectName() } + }; + let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); + + host.reloadFS([file1, file2, config, libFile]); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + checkDiagnosticsWithLinePos(diags, []); + }); + + it("configured projects - diagnostics for corrupted config 1", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/lib.ts", + content: "" + }; + const correctConfig = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) + }; + const corruptedConfig = { + path: correctConfig.path, + content: correctConfig.content.substr(1) + }; + const host = createServerHost([file1, file2, corruptedConfig]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, [ + "'{' expected." + ]); + assert.isNotNull(projectErrors[0].file); + assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); + } + // fix config and trigger watcher + host.reloadFS([file1, file2, correctConfig]); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); + } + }); + + it("configured projects - diagnostics for corrupted config 2", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/lib.ts", + content: "" + }; + const correctConfig = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) + }; + const corruptedConfig = { + path: correctConfig.path, + content: correctConfig.content.substr(1) + }; + const host = createServerHost([file1, file2, correctConfig]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); + } + // break config and trigger watcher + host.reloadFS([file1, file2, corruptedConfig]); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, [ + "'{' expected." + ]); + assert.isNotNull(projectErrors[0].file); + assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); + } + }); + }); + + describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => { + function createErrorLogger() { + let hasError = false; + const errorLogger: server.Logger = { + close: noop, + hasLevel: () => true, + loggingEnabled: () => true, + perftrc: noop, + info: noop, + msg: (_s, type) => { + if (type === server.Msg.Err) { + hasError = true; + } + }, + startGroup: noop, + endGroup: noop, + getLogFileName: () => undefined + }; + return { + errorLogger, + hasError: () => hasError + }; + } + + it("document is not contained in project", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: "/a/b/tsconfig.json", + content: "{" + }; + const host = createServerHost([file1, corruptedConfig]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + + const project = projectService.findProject(corruptedConfig.path)!; + checkProjectRootFiles(project, [file1.path]); + }); + + describe("when opening new file that doesnt exist on disk yet", () => { + function verifyNonExistentFile(useProjectRoot: boolean) { + const folderPath = "/user/someuser/projects/someFolder"; + const fileInRoot: File = { + path: `/src/somefile.d.ts`, + content: "class c { }" + }; + const fileInProjectRoot: File = { + path: `${folderPath}/src/somefile.d.ts`, + content: "class c { }" + }; + const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); + const { hasError, errorLogger } = createErrorLogger(); + const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true }); + + const projectService = session.getProjectService(); + const untitledFile = "untitled:Untitled-1"; + const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; + const refPathNotFound2 = "./src/somefile.d.ts"; + const fileContent = `/// +/// `; + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: untitledFile, + fileContent, + scriptKindName: "TS", + projectRootPath: useProjectRoot ? folderPath : undefined + } + }); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path); + const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path); + const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath(`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path); + const infoForSomefileAtRoot = projectService.getScriptInfoForPath(`${fileInRoot.path.toLowerCase()}` as Path); + if (useProjectRoot) { + assert.isDefined(infoForUntitledAtProjectRoot); + assert.isUndefined(infoForUnitiledAtRoot); + } + else { + assert.isDefined(infoForUnitiledAtRoot); + assert.isUndefined(infoForUntitledAtProjectRoot); + } + assert.isUndefined(infoForSomefileAtRoot); + assert.isUndefined(infoForSomefileAtProjectRoot); + + // Since this is not js project so no typings are queued + host.checkTimeoutQueueLength(0); + + const newTimeoutId = host.getNextTimeoutId(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [untitledFile] + } + }); + host.checkTimeoutQueueLength(1); + + // Run the last one = get error request + host.runQueuedTimeoutCallbacks(newTimeoutId); + + assert.isFalse(hasError()); + host.checkTimeoutQueueLength(0); + checkErrorMessage(session, "syntaxDiag", { file: untitledFile, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + assert.isFalse(hasError()); + const errorOffset = fileContent.indexOf(refPathNotFound1) + 1; + checkErrorMessage(session, "semanticDiag", { + file: untitledFile, + diagnostics: [ + createDiagnostic({ line: 1, offset: errorOffset }, { line: 1, offset: errorOffset + refPathNotFound1.length }, Diagnostics.File_0_not_found, [refPathNotFound1], "error"), + createDiagnostic({ line: 2, offset: errorOffset }, { line: 2, offset: errorOffset + refPathNotFound2.length }, Diagnostics.File_0_not_found, [refPathNotFound2.substr(2)], "error") + ] + }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + assert.isFalse(hasError()); + checkErrorMessage(session, "suggestionDiag", { file: untitledFile, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + } + + it("has projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ true); + }); + + it("does not have projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ false); + }); + }); + + it("folder rename updates project structure and reports no errors", () => { + const projectDir = "/a/b/projects/myproject"; + const app: File = { + path: `${projectDir}/bar/app.ts`, + content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" + }; + const foo: File = { + path: `${projectDir}/foo/foo.ts`, + content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" + }; + const configFile: File = { + path: `${projectDir}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) + }; + const host = createServerHost([app, foo, configFile]); + const session = createSession(host, { canUseEvents: true, }); + const projectService = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: app.path, } + }); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isDefined(projectService.configuredProjects.get(configFile.path)); + verifyErrorsInApp(); + + host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); + host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); + verifyErrorsInApp(); + + function verifyErrorsInApp() { + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [app.path] + } + }); + host.checkTimeoutQueueLengthAndRun(1); + checkErrorMessage(session, "syntaxDiag", { file: app.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + checkErrorMessage(session, "semanticDiag", { file: app.path, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: app.path, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + } + }); + + it("Getting errors before opening file", () => { + const file: File = { + path: "/a/b/project/file.ts", + content: "let x: number = false;" + }; + const host = createServerHost([file, libFile]); + const { hasError, errorLogger } = createErrorLogger(); + const session = createSession(host, { canUseEvents: true, logger: errorLogger }); + + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path] + } + }); + + host.runQueuedImmediateCallbacks(); + assert.isFalse(hasError()); + checkCompleteEvent(session, 1, expectedSequenceId); + session.clearMessages(); + }); + + it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { + const projectRoot = "/user/username/projects/myproject"; + const app: File = { + path: `${projectRoot}/src/client/app.js`, + content: "" + }; + const serverUtilities: File = { + path: `${projectRoot}/src/server/utilities.js`, + content: `function getHostName() { return "hello"; } export { getHostName };` + }; + const backendTest: File = { + path: `${projectRoot}/test/backend/index.js`, + content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` + }; + const files = [libFile, app, serverUtilities, backendTest]; + const host = createServerHost(files); + const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true }); + openFilesForSession([{ file: app, projectRootPath: projectRoot }], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + checkProjectActualFiles(project, [libFile.path, app.path]); + openFilesForSession([{ file: backendTest, projectRootPath: projectRoot }], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, files.map(f => f.path)); + checkErrors([backendTest.path, app.path]); + closeFilesForSession([backendTest], session); + openFilesForSession([{ file: serverUtilities.path, projectRootPath: projectRoot }], session); + checkErrors([serverUtilities.path, app.path]); + + function checkErrors(openFiles: [string, string]) { + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.Geterr, + arguments: { + delay: 0, + files: openFiles + } + }); + + for (const openFile of openFiles) { + session.clearMessages(); + host.checkTimeoutQueueLength(3); + host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); + + checkErrorMessage(session, "syntaxDiag", { file: openFile, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(); + checkErrorMessage(session, "semanticDiag", { file: openFile, diagnostics: [] }); + session.clearMessages(); + + host.runQueuedImmediateCallbacks(1); + checkErrorMessage(session, "suggestionDiag", { file: openFile, diagnostics: [] }); + } + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); + } + }); + }); + + describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { + function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic { + const d = Diagnostics.Unknown_compiler_option_0; + const start = configFile.content.indexOf(prop) - 1; // start at "prop" + return { + fileName: configFile.path, + start, + length: prop.length + 2, + messageText: formatStringFromArgs(d.message, [prop]), + category: d.category, + code: d.code, + reportsUnnecessary: undefined + }; + } + + function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic { + const findString = `{"path":"./${relativeFileName}"}`; + const d = Diagnostics.File_0_not_found; + const start = configFile.content.indexOf(findString); + return { + fileName: configFile.path, + start, + length: findString.length, + messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]), + category: d.category, + code: d.code, + reportsUnnecessary: undefined + }; + } + + it("are generated when the config file has errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ + getUnknownCompilerOptionDiagnostic(configFile, "foo"), + getUnknownCompilerOptionDiagnostic(configFile, "allowJS") + ]); + }); + + it("are generated when the config file doesn't have errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); + }); + + it("are generated when the config file changes", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const files = [file, libFile, configFile]; + const serverEventManager = new TestServerEventManager(files); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); + + configFile.content = `{ + "compilerOptions": { + "haha": 123 + } + }`; + serverEventManager.host.reloadFS(files); + serverEventManager.host.runQueuedTimeoutCallbacks(); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [ + getUnknownCompilerOptionDiagnostic(configFile, "haha") + ]); + + configFile.content = `{ + "compilerOptions": {} + }`; + serverEventManager.host.reloadFS(files); + serverEventManager.host.runQueuedTimeoutCallbacks(); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray); + }); + + it("are not generated when the config file does not include file opened and config file has errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + }, + "files": ["app.ts"] + }` + }; + const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + openFilesForSession([file2], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true); + openFilesForSession([file], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["app.ts"] + }` + }; + + const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + openFilesForSession([file2], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("contains the project reference errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const noSuchTsconfig = "no-such-tsconfig.json"; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "files": ["app.ts"], + "references": [{"path":"./${noSuchTsconfig}"}] + }` + }; + + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ + getFileNotFoundDiagnostic(configFile, noSuchTsconfig) + ]); + }); + }); + + describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => { + it("for inferred project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + openFilesForSession([f1], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const projectName = projectService.inferredProjects[0].getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: projectName } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsForInferredProjects, + seq: 3, + arguments: { options: { module: ModuleKind.CommonJS } } + }); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName: projectName } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterUpdate.length === 0); + }); + + it("for external project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/project.csproj"; + const externalFiles = toExternalFiles([f1.path]); + projectService.openExternalProject({ + projectFileName, + rootFiles: externalFiles, + options: {} + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 0); + + session.executeCommand({ + type: "request", + command: server.CommandNames.OpenExternalProject, + seq: 3, + arguments: { + projectFileName, + rootFiles: externalFiles, + options: { module: ModuleKind.CommonJS } + } + }); + const diagsAfterUpdate = session.executeCommand({ + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterUpdate.length === 0); + }); + }); + + describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { + it("when options change", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFileContentBeforeComment = `{`; + const configFileContentComment = ` + // comment`; + const configFileContentAfterComment = ` + "compilerOptions": { + "allowJs": true, + "declaration": true + } + }`; + const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; + const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; + + const configFile = { + path: "/a/b/tsconfig.json", + content: configFileContentWithComment + }; + const host = createServerHost([file, libFile, configFile]); + const session = createSession(host); + openFilesForSession([file], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const projectName = configuredProjectAt(projectService, 0).getProjectName(); + + const diags = session.executeCommand({ + type: "request", + command: server.CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + }).response as ReadonlyArray; + assert.isTrue(diags.length === 2); + + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS([file, configFile]); + + const diagsAfterEdit = session.executeCommand({ + type: "request", + command: server.CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + }).response as ReadonlyArray; + assert.isTrue(diagsAfterEdit.length === 2); + + verifyDiagnostic(diags[0], diagsAfterEdit[0]); + verifyDiagnostic(diags[1], diagsAfterEdit[1]); + + function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { + assert.equal(beforeEditDiag.message, afterEditDiag.message); + assert.equal(beforeEditDiag.code, afterEditDiag.code); + assert.equal(beforeEditDiag.category, afterEditDiag.category); + assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); + assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); + assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); + assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); + } + }); + }); + + describe("unittests:: tsserver:: Project Errors with config file change", () => { + it("Updates diagnostics when '--noUnusedLabels' changes", () => { + const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; + const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; + const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; + + const host = createServerHost([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + + host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); + host.runQueuedTimeoutCallbacks(); + + const response = executeSessionRequest(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined; + assert.deepEqual(response, [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 + "label".length }, + text: "Unused label.", + category: "error", + code: Diagnostics.Unused_label.code, + relatedInformation: undefined, + reportsUnnecessary: true, + source: undefined, + }, + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts new file mode 100644 index 00000000000..e39c510216d --- /dev/null +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -0,0 +1,658 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: with project references and tsbuild", () => { + function createHost(files: ReadonlyArray, rootNames: ReadonlyArray) { + const host = createServerHost(files); + + // ts build should succeed + const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {}); + solutionBuilder.buildAllProjects(); + assert.equal(host.getOutput().length, 0); + + return host; + } + + describe("with container project", () => { + function getProjectFiles(project: string): [File, File] { + return [ + TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), + TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"), + ]; + } + + const project = "container"; + const containerLib = getProjectFiles("container/lib"); + const containerExec = getProjectFiles("container/exec"); + const containerCompositeExec = getProjectFiles("container/compositeExec"); + const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); + const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; + + it("does not error on container only project", () => { + const host = createHost(files, [containerConfig.path]); + + // Open external project for the folder + const session = createSession(host); + const service = session.getProjectService(); + service.openExternalProjects([{ + projectFileName: TestFSWithWatch.getTsBuildProjectFilePath(project, project), + rootFiles: files.map(f => ({ fileName: f.path })), + options: {} + }]); + checkNumberOfProjects(service, { configuredProjects: 4 }); + files.forEach(f => { + const args: protocol.FileRequestArgs = { + file: f.path, + projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined + }; + const syntaxDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: args + }).response; + assert.deepEqual(syntaxDiagnostics, []); + const semanticDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: args + }).response; + assert.deepEqual(semanticDiagnostics, []); + }); + const containerProject = service.configuredProjects.get(containerConfig.path)!; + checkProjectActualFiles(containerProject, [containerConfig.path]); + const optionsDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull, + arguments: { projectFileName: containerProject.projectName } + }).response; + assert.deepEqual(optionsDiagnostics, []); + }); + + it("can successfully find references with --out options", () => { + const host = createHost(files, [containerConfig.path]); + const session = createSession(host); + openFilesForSession([containerCompositeExec[1]], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.Rename, + arguments: { + file: containerCompositeExec[1].path, + ...locationOfMyConst + } + }).response as protocol.RenameResponseBody; + + + const myConstLen = "myConst".length; + const locationOfMyConstInLib = protocolLocationFromSubstring(containerLib[1].content, "myConst"); + assert.deepEqual(response.locs, [ + { file: containerCompositeExec[1].path, locs: [{ start: locationOfMyConst, end: { line: locationOfMyConst.line, offset: locationOfMyConst.offset + myConstLen } }] }, + { file: containerLib[1].path, locs: [{ start: locationOfMyConstInLib, end: { line: locationOfMyConstInLib.line, offset: locationOfMyConstInLib.offset + myConstLen } }] } + ]); + }); + }); + + describe("with main and depedency project", () => { + const projectLocation = "/user/username/projects/myproject"; + const dependecyLocation = `${projectLocation}/dependency`; + const mainLocation = `${projectLocation}/main`; + const dependencyTs: File = { + path: `${dependecyLocation}/FnS.ts`, + content: `export function fn1() { } +export function fn2() { } +export function fn3() { } +export function fn4() { } +export function fn5() { } +` + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } }) + }; + + const mainTs: File = { + path: `${mainLocation}/main.ts`, + content: `import { + fn1, + fn2, + fn3, + fn4, + fn5 +} from '../dependency/fns' + +fn1(); +fn2(); +fn3(); +fn4(); +fn5(); +` + }; + const mainConfig: File = { + path: `${mainLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true }, + references: [{ path: "../dependency" }] + }) + }; + + const randomFile: File = { + path: `${projectLocation}/random/random.ts`, + content: "let a = 10;" + }; + const randomConfig: File = { + path: `${projectLocation}/random/tsconfig.json`, + content: "{}" + }; + const dtsLocation = `${dependecyLocation}/FnS.d.ts`; + const dtsPath = dtsLocation.toLowerCase() as Path; + const dtsMapLocation = `${dtsLocation}.map`; + const dtsMapPath = dtsMapLocation.toLowerCase() as Path; + + const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig]; + + function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray, closedInfos: ReadonlyArray, otherWatchedFiles: ReadonlyArray) { + checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos)); + checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase())); + } + + function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray, closedInfos: ReadonlyArray, otherWatchedFiles: ReadonlyArray) { + verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path)); + } + + function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) { + verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]); + } + + // Returns request and expected Response, expected response when no map file + interface SessionAction { + reqName: string; + request: Partial; + expectedResponse: Response; + expectedResponseNoMap?: Response; + expectedResponseNoDts?: Response; + } + function gotoDefintinionFromMainTs(fn: number): SessionAction { + const textSpan = usageSpan(fn); + const definition: protocol.FileSpan = { file: dependencyTs.path, ...definitionSpan(fn) }; + const declareSpaceLength = "declare ".length; + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To dependency + definitions: [definition], + textSpan + }, + expectedResponseNoMap: { + // To the dts + definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength } }], + textSpan + }, + expectedResponseNoDts: { + // To import declaration + definitions: [{ file: mainTs.path, ...importSpan(fn) }], + textSpan + } + }; + } + + function definitionSpan(fn: number): protocol.TextSpan { + return { start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } }; + } + function importSpan(fn: number): protocol.TextSpan { + return { start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } }; + } + function usageSpan(fn: number): protocol.TextSpan { + return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } }; + } + + function renameFromDependencyTs(fn: number): SessionAction { + const triggerSpan = definitionSpan(fn); + return { + reqName: "rename", + request: { + command: protocol.CommandTypes.Rename, + arguments: { file: dependencyTs.path, ...triggerSpan.start } + }, + expectedResponse: { + info: { + canRename: true, + fileToRename: undefined, + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + triggerSpan + }, + locs: [ + { file: dependencyTs.path, locs: [triggerSpan] } + ] + } + }; + } + + function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction { + const { reqName, request, expectedResponse } = renameFromDependencyTs(fn); + const { info, locs } = expectedResponse; + return { + reqName, + request, + expectedResponse: { + info, + locs: [ + locs[0], + { + file: mainTs.path, + locs: [ + importSpan(fn), + usageSpan(fn) + ] + } + ] + }, + // Only dependency result + expectedResponseNoMap: expectedResponse, + expectedResponseNoDts: expectedResponse + }; + } + + // Returns request and expected Response + type SessionActionGetter = (fn: number) => SessionAction; + // Open File, expectedProjectActualFiles, actionGetter, openFileLastLine + interface DocumentPositionMapperVerifier { + openFile: File; + expectedProjectActualFiles: ReadonlyArray; + actionGetter: SessionActionGetter; + openFileLastLine: number; + } + function verifyDocumentPositionMapperUpdates( + mainScenario: string, + verifier: ReadonlyArray, + closedInfos: ReadonlyArray) { + + const openFiles = verifier.map(v => v.openFile); + const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles); + const actionGetters = verifier.map(v => v.actionGetter); + const openFileLastLines = verifier.map(v => v.openFileLastLine); + + const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`); + const openInfos = openFiles.map(f => f.path); + // When usage and dependency are used, dependency config is part of closedInfo so ignore + const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles; + function openTsFile(onHostCreate?: (host: TestServerHost) => void) { + const host = createHost(files, [mainConfig.path]); + if (onHostCreate) { + onHostCreate(host); + } + const session = createSession(host); + openFilesForSession([...openFiles, randomFile], session); + return { host, session }; + } + + function checkProject(session: TestSession, noDts?: true) { + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length }); + configFiles.forEach((configFile, index) => { + checkProjectActualFiles( + service.configuredProjects.get(configFile)!, + noDts ? + expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) : + expectedProjectActualFiles[index] + ); + }); + } + + function verifyInfos(session: TestSession, host: TestServerHost) { + verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles); + } + + function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) { + const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); + verifyInfosWithRandom( + session, + host, + openInfos, + closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)), + dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles + ); + } + + function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { + const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); + const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined); + verifyInfosWithRandom( + session, + host, + openInfos, + closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)), + // When project actual file contains dts, it needs to be watched + dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ? + otherWatchedFiles.concat(dtsClosedInfo) : + otherWatchedFiles + ); + } + + function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) { + assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap); + if (notEqual) { + assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); + } + else { + assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper); + } + } + + function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) { + const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn); + const { response } = session.executeCommandSeq(request); + return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts }; + } + + function firstAction(session: TestSession) { + actionGetters.forEach(actionGetter => action(actionGetter, 1, session)); + } + + function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) { + // action + let isFirst = true; + for (const actionGetter of actionGetters) { + for (let fn = 1; fn <= 5; fn++) { + const result = action(actionGetter, fn, session); + const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath); + if (dtsAbsent) { + assert.isUndefined(dtsInfo); + } + else { + assert.isDefined(dtsInfo); + } + verifyAction(result, dtsInfo, isFirst); + isFirst = false; + } + } + } + + function verifyAllFnAction( + session: TestSession, + host: TestServerHost, + firstDocumentPositionMapperNotEquals?: true, + dependencyMap?: server.ScriptInfo, + documentPositionMapper?: server.ScriptInfo["documentPositionMapper"] + ) { + // action + verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => { + assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`); + verifyInfos(session, host); + assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath); + if (isFirst) { + if (dependencyMap) { + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals); + documentPositionMapper = dependencyMap.documentPositionMapper; + } + else { + dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; + documentPositionMapper = dependencyMap.documentPositionMapper; + } + } + else { + verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper); + } + }); + return { dependencyMap: dependencyMap!, documentPositionMapper }; + } + + function verifyAllFnActionWithNoMap( + session: TestSession, + host: TestServerHost, + dependencyTsOK?: true + ) { + let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"]; + // action + verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => { + assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`); + verifyInfosWhenNoMapFile(session, host, dependencyTsOK); + assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath)); + if (isFirst) { + assert.isNotString(dtsInfo!.sourceMapFilePath); + assert.isNotFalse(dtsInfo!.sourceMapFilePath); + assert.isDefined(dtsInfo!.sourceMapFilePath); + sourceMapFilePath = dtsInfo!.sourceMapFilePath; + } + else { + assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath); + } + }); + return sourceMapFilePath; + } + + function verifyAllFnActionWithNoDts( + session: TestSession, + host: TestServerHost, + dependencyTsAndMapOk?: true + ) { + // action + verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => { + assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`); + verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk); + }, /*dtsAbsent*/ true); + } + + function verifyScenarioWithChangesWorker( + change: (host: TestServerHost, session: TestSession) => void, + afterActionDocumentPositionMapperNotEquals: true | undefined, + timeoutBeforeAction: boolean + ) { + const { host, session } = openTsFile(); + + // Create DocumentPositionMapper + firstAction(session); + const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!; + const documentPositionMapper = dependencyMap.documentPositionMapper; + + // change + change(host, session); + if (timeoutBeforeAction) { + host.runQueuedTimeoutCallbacks(); + checkProject(session); + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); + } + + // action + verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper); + } + + function verifyScenarioWithChanges( + scenarioName: string, + change: (host: TestServerHost, session: TestSession) => void, + afterActionDocumentPositionMapperNotEquals?: true + ) { + describe(scenarioName, () => { + it("when timeout occurs before request", () => { + verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true); + }); + + it("when timeout does not occur before request", () => { + verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false); + }); + }); + } + + function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) { + // Main scenario action + const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host); + checkProject(session); + verifyInfos(session, host); + + // Collecting at this point retains dependency.d.ts and map + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfos(session, host); + verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) { + // Main scenario action + verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario); + + // Collecting at this point retains dependency.d.ts and map watcher + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfosWhenNoMapFile(session, host); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) { + // Main scenario action + verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk); + + // Collecting at this point retains dependency.d.ts and map watcher + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfosWhenNoDtsFile(session, host); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + + function verifyScenarioWhenFileNotPresent( + scenarioName: string, + fileLocation: string, + verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void, + noDts?: true + ) { + describe(scenarioName, () => { + it(mainScenario, () => { + const { host, session } = openTsFile(host => host.deleteFile(fileLocation)); + checkProject(session, noDts); + + verifyScenarioAndScriptInfoCollection(session, host); + }); + + it("when file is created", () => { + let fileContents: string | undefined; + const { host, session } = openTsFile(host => { + fileContents = host.readFile(fileLocation); + host.deleteFile(fileLocation); + }); + firstAction(session); + + host.writeFile(fileLocation, fileContents!); + verifyMainScenarioAndScriptInfoCollection(session, host); + }); + + it("when file is deleted", () => { + const { host, session } = openTsFile(); + firstAction(session); + + // The dependency file is deleted when orphan files are collected + host.deleteFile(fileLocation); + verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true); + }); + }); + } + + it(mainScenario, () => { + const { host, session } = openTsFile(); + checkProject(session); + + verifyMainScenarioAndScriptInfoCollection(session, host); + }); + + // Edit + verifyScenarioWithChanges( + "when usage file changes, document position mapper doesnt change", + (_host, session) => openFiles.forEach( + (openFile, index) => session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" } + }) + ) + ); + + // Edit dts to add new fn + verifyScenarioWithChanges( + "when dependency .d.ts changes, document position mapper doesnt change", + host => host.writeFile( + dtsLocation, + host.readFile(dtsLocation)!.replace( + "//# sourceMappingURL=FnS.d.ts.map", + `export declare function fn6(): void; +//# sourceMappingURL=FnS.d.ts.map` + ) + ) + ); + + // Edit map file to represent added new line + verifyScenarioWithChanges( + "when dependency file's map changes", + host => host.writeFile( + dtsMapLocation, + `{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}` + ), + /*afterActionDocumentPositionMapperNotEquals*/ true + ); + + verifyScenarioWhenFileNotPresent( + "when map file is not present", + dtsMapLocation, + verifyMainScenarioAndScriptInfoCollectionWithNoMap + ); + + verifyScenarioWhenFileNotPresent( + "when .d.ts file is not present", + dtsLocation, + verifyMainScenarioAndScriptInfoCollectionWithNoDts, + /*noDts*/ true + ); + } + + const usageVerifier: DocumentPositionMapperVerifier = { + openFile: mainTs, + expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath], + actionGetter: gotoDefintinionFromMainTs, + openFileLastLine: 14 + }; + describe("from project that uses dependency", () => { + const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation]; + verifyDocumentPositionMapperUpdates( + "can go to definition correctly", + [usageVerifier], + closedInfos + ); + }); + + const definingVerifier: DocumentPositionMapperVerifier = { + openFile: dependencyTs, + expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path], + actionGetter: renameFromDependencyTs, + openFileLastLine: 6 + }; + describe("from defining project", () => { + const closedInfos = [libFile.path, dtsLocation, dtsMapLocation]; + verifyDocumentPositionMapperUpdates( + "rename locations from dependency", + [definingVerifier], + closedInfos + ); + }); + + describe("when opening depedency and usage project", () => { + const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path]; + verifyDocumentPositionMapperUpdates( + "goto Definition in usage and rename locations from defining project", + [usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }], + closedInfos + ); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts new file mode 100644 index 00000000000..00941a1d3c9 --- /dev/null +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -0,0 +1,1426 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: Projects", () => { + it("handles the missing files - that were added to program because they were added with /// { + const file1: File = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + checkNumberOfInferredProjects(projectService, 1); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path]); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + + // Two errors: CommonFile2 not found and cannot find name y + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, + { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } + ]); + + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfInferredProjects(projectService, 1); + assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; + verifyNoDiagnostics(diags); + }); + + it("should create new inferred projects for files excluded from a configured project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + configFile.content = `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}"] + }`; + host.reloadFS(files); + + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path]); + + projectService.openClientFile(commonFile2.path); + checkNumberOfInferredProjects(projectService, 1); + }); + + it("should disable features when the files are too large", () => { + const file1 = { + path: "/a/b/f1.js", + content: "let x =1;", + fileSize: 10 * 1024 * 1024 + }; + const file2 = { + path: "/a/b/f2.js", + content: "let y =1;", + fileSize: 6 * 1024 * 1024 + }; + const file3 = { + path: "/a/b/f3.js", + content: "let y =1;", + fileSize: 6 * 1024 * 1024 + }; + + const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; + + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); + const proj1 = projectService.findProject(proj1name)!; + assert.isTrue(proj1.languageServiceEnabled); + + projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); + const proj2 = projectService.findProject(proj2name)!; + assert.isTrue(proj2.languageServiceEnabled); + + projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); + const proj3 = projectService.findProject(proj3name)!; + assert.isFalse(proj3.languageServiceEnabled); + }); + + describe("ignoreConfigFiles", () => { + it("external project including config file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + + const externalProjectName = "externalproject"; + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, config1.path]), + options: {}, + projectFileName: externalProjectName + }); + + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const proj = projectService.externalProjects[0]; + assert.isDefined(proj); + + assert.isTrue(proj.fileExists(file1.path)); + }); + + it("loose file included in config file (openClientFile)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openClientFile(file1.path, file1.content); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + + assert.isTrue(proj.fileExists(file1.path)); + }); + + it("loose file included in config file (applyCodeChanges)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify( + { + compilerOptions: {}, + files: ["f1.ts"] + } + ) + }; + + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.applyChangesInOpenFiles([{ fileName: file1.path, content: file1.content }], [], []); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + + assert.isTrue(proj.fileExists(file1.path)); + }); + }); + + it("reload regular file after closing", () => { + const f1 = { + path: "/a/b/app.ts", + content: "x." + }; + const f2 = { + path: "/a/b/lib.ts", + content: "let x: number;" + }; + + const host = createServerHost([f1, f2, libFile]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); + + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let x: string"); + + service.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); + + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; + // should contain completions for string + assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); + assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); + + service.closeClientFile(f2.path); + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; + // should contain completions for string + assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); + assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); + }); + + it("clear mixed content file after closing", () => { + const f1 = { + path: "/a/b/app.ts", + content: " " + }; + const f2 = { + path: "/a/b/lib.html", + content: "" + }; + + const host = createServerHost([f1, f2, libFile]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); + + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let somelongname: string"); + + service.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); + + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; + assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); + + service.closeClientFile(f2.path); + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; + assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); + const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!; + assert.equal(sf2.text, ""); + }); + + it("changes in closed files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const inferredProject0 = projectService.inferredProjects[0]; + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + const inferredProject1 = projectService.inferredProjects[1]; + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + + host.reloadFS([file1, modifiedFile2, file3]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + }); + + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(2); + + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + }); + + it("ignores files excluded by a custom safe type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: "export let x = 5" + }; + const office = { + path: "/lib/duckquack-3.min.js", + content: "whoa do @@ not parse me ok thanks!!!" + }; + const host = createServerHost([file1, office, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); + } finally { + projectService.resetSafeList(); + } + }); + + it("file with name constructor.js doesnt cause issue with typeAcquisition when safe type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: `export let x = 5; import { s } from "s"` + }; + const constructorFile = { + path: "/a/b/constructor.js", + content: "const x = 10;" + }; + const bliss = { + path: "/a/b/bliss.js", + content: "export function is() { return true; }" + }; + const host = createServerHost([file1, libFile, constructorFile, bliss, customTypesMap]); + let request: string | undefined; + const cachePath = "/a/data"; + const typingsInstaller: server.ITypingsInstaller = { + isKnownTypesPackageName: returnFalse, + installPackage: notImplemented, + inspectValue: notImplemented, + enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => { + assert.isUndefined(request); + request = JSON.stringify(server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || server.emptyArray, cachePath)); + }, + attach: noop, + onProjectClosed: noop, + globalTypingsCacheLocation: cachePath + }; + + const projectName = "project"; + const projectService = createProjectService(host, { typingsInstaller }); + projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: toExternalFiles([file1.path, constructorFile.path, bliss.path]) }); + assert.equal(request, JSON.stringify({ + projectName, + fileNames: [libFile.path, file1.path, constructorFile.path, bliss.path], + compilerOptions: { allowNonTsExtensions: true, noEmitForJsFiles: true }, + typeAcquisition: { include: ["blissfuljs"], exclude: [], enable: true }, + unresolvedImports: ["s"], + projectRootPath: "/", + cachePath, + kind: "discover" + })); + const response = JSON.parse(request!); + request = undefined; + projectService.updateTypingsForProject({ + kind: "action::set", + projectName: response.projectName, + typeAcquisition: response.typeAcquisition, + compilerOptions: response.compilerOptions, + typings: emptyArray, + unresolvedImports: response.unresolvedImports, + }); + + host.checkTimeoutQueueLengthAndRun(2); + assert.isUndefined(request); + }); + + it("ignores files excluded by the default type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: "export let x = 5" + }; + const minFile = { + path: "/c/moment.min.js", + content: "unspecified" + }; + const kendoFile1 = { + path: "/q/lib/kendo/kendo.all.min.js", + content: "unspecified" + }; + const kendoFile2 = { + path: "/q/lib/kendo/kendo.ui.min.js", + content: "unspecified" + }; + const kendoFile3 = { + path: "/q/lib/kendo-ui/kendo.all.js", + content: "unspecified" + }; + const officeFile1 = { + path: "/scripts/Office/1/excel-15.debug.js", + content: "unspecified" + }; + const officeFile2 = { + path: "/scripts/Office/1/powerpoint.js", + content: "unspecified" + }; + const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2]; + const host = createServerHost(files); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]); + } finally { + projectService.resetSafeList(); + } + }); + + it("removes version numbers correctly", () => { + const testData: [string, string][] = [ + ["jquery-max", "jquery-max"], + ["jquery.min", "jquery"], + ["jquery-min.4.2.3", "jquery"], + ["jquery.min.4.2.1", "jquery"], + ["minimum", "minimum"], + ["min", "min"], + ["min.3.2", "min"], + ["jquery", "jquery"] + ]; + for (const t of testData) { + assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); + } + }); + + it("ignores files excluded by a legacy safe type list", () => { + const file1 = { + path: "/a/b/bliss.js", + content: "let x = 5" + }; + const file2 = { + path: "/a/b/foo.js", + content: "" + }; + const file3 = { + path: "/a/b/Bacon.js", + content: "let y = 5" + }; + const host = createServerHost([file1, file2, file3, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(), [file2.path]); + } finally { + projectService.resetSafeList(); + } + }); + + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + let inferredProjects = projectService.inferredProjects.slice(); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]); + checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + + projectService.closeClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + assert.strictEqual(projectService.inferredProjects[2], inferredProjects[2]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + assert.isTrue(projectService.inferredProjects[2].isOrphan()); + + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); + + it("regression test for crash in acquireOrUpdateDocument", () => { + const tsFile = { + fileName: "/a/b/file1.ts", + path: "/a/b/file1.ts", + content: "" + }; + const jsFile = { + path: "/a/b/file1.js", + content: "var x = 10;", + fileName: "/a/b/file1.js", + scriptKind: "JS" as "JS" + }; + + const host = createServerHost([]); + const projectService = createProjectService(host); + projectService.applyChangesInOpenFiles([tsFile], [], []); + const projs = projectService.synchronizeProjectList([]); + projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); + projectService.synchronizeProjectList([projs[0].info!]); + projectService.applyChangesInOpenFiles([jsFile], [], []); + }); + + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createServerHost([file1, file2, config]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + host.reloadFS([file1, file2]); + host.checkTimeoutQueueLengthAndRun(1); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); + + it("loading files with correct priority", () => { + const f1 = { + path: "/a/main.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/main.js", + content: "var y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { allowJs: true } + }) + }; + const host = createServerHost([f1, f2, config]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ + extraFileExtensions: [ + { extension: ".js", isMixedContent: false }, + { extension: ".html", isMixedContent: true } + ] + }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); + + // Should close configured project with next file open + projectService.closeClientFile(f1.path); + + projectService.openClientFile(f2.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config.path)); + checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); + }); + + it("tsconfig script block support", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([file1, file2, config]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + // HTML file will not be included in any projects yet + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const configuredProj = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configuredProj, [file1.path, config.path]); + + // Specify .html extension as mixed content + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + + // The configured project should now be updated to include html file + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Open HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], + /*changedFiles*/ undefined, + /*closedFiles*/ undefined); + + // Now HTML file is included in the project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Check identifiers defined in HTML content are available in .ts file + const project = configuredProjectAt(projectService, 0); + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions); + assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); + + // Close HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ undefined, + /*closedFiles*/[file2.path]); + + // HTML file is still included in project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + + // Check identifiers defined in HTML content are not available in .ts file + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions); + assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); + }); + + it("no tsconfig script block diagnostic errors", () => { + + // #1. Ensure no diagnostic errors when allowJs is true + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + + let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + let session = createSession(host); + + // Specify .html extension as mixed content in a configure host request + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + let projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #2. Ensure no errors when allowJs is false + const config2 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: false } }) + }; + + host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #3. Ensure no errors when compiler options aren't specified + const config3 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; + + host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #4. Ensure no errors when files are explicitly specified in tsconfig + const config4 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] }) + }; + + host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + + // #4. Ensure no errors when files are explicitly excluded in tsconfig + const config5 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] }) + }; + + host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + + session.executeCommand(configureHostRequest); + + openFilesForSession([file1], session); + projectService = session.getProjectService(); + + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); + + it("project structure update is deferred if files are not added\removed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `import {x} from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let x = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/[{ fileName: file1.path, changes: [{ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }] }], + /*closedFiles*/ undefined); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.ensureInferredProjectsUpToDate_TestOnly(); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); + + it("files with mixed content are handled correctly", () => { + const file1 = { + path: "/a/b/f1.html", + content: `