diff --git a/Jakefile.js b/Jakefile.js index cb53b479502e6..575f293a6235b 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -128,7 +128,8 @@ var harnessSources = [ "services/preProcessFile.ts", "services/patternMatcher.ts", "versionCache.ts", - "convertToBase64.ts" + "convertToBase64.ts", + "expandFiles.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ @@ -505,7 +506,9 @@ function cleanTestDirs() { // used to pass data from jake command line directly to run.js function writeTestConfigFile(tests, testConfigFile) { console.log('Running test(s): ' + tests); - var testConfigContents = '{\n' + '\ttest: [\'' + tests + '\']\n}'; + var testConfigContents = JSON.stringify({ + "test": [tests] + }); fs.writeFileSync('test.config', testConfigContents); } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 7a589fe61cb3b..a168f363e17f2 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -331,6 +331,11 @@ module ts { return { error: createCompilerDiagnostic(Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } + + const enum ExpandResult { + Ok, + Error + } /** * Parse the contents of a config file (tsconfig.json). @@ -339,11 +344,12 @@ module ts { * file to. e.g. outDir */ export function parseConfigFile(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine { - var errors: Diagnostic[] = []; - + let errors: Diagnostic[] = []; + let options = getCompilerOptions(); + return { - options: getCompilerOptions(), - fileNames: getFiles(), + options, + fileNames: getFileNames(), errors }; @@ -389,23 +395,449 @@ module ts { return options; } - function getFiles(): string[] { - var files: string[] = []; + function getFileNames(): string[] { + let fileNames: string[]; if (hasProperty(json, "files")) { - if (json["files"] instanceof Array) { - var files = map(json["files"], s => combinePaths(basePath, s)); + fileNames = json["files"] instanceof Array ? json["files"] : []; + } + + let includeSpecs = json["include"] instanceof Array ? json["include"] : undefined; + if (!fileNames && !includeSpecs) { + includeSpecs = ["**/*.ts"]; + } + + let excludeSpecs = json["exclude"] instanceof Array ? json["exclude"] : undefined; + return expandFiles(basePath, fileNames, includeSpecs, excludeSpecs, options, host, errors); + } + } + + /** + * Expands an array of file specifications. + * @param basePath The base path for any relative file specifications. + * @param fileNames The literal file names to include. + * @param includeSpecs The file specifications to expand. + * @param excludeSpecs The file specifications to exclude. + * @param options Compiler options. + * @param host The host used to resolve files and directories. + * @param errors An array for diagnostic reporting. + */ + export function expandFiles(basePath: string, fileNames: string[], includeSpecs: string[], excludeSpecs: string[], options: CompilerOptions, host: ParseConfigHost, errors?: Diagnostic[]): string[] { + const wildcardCharacterPattern = /[\*\?\[]/g; + const reservedCharacterPattern = /[^\w\-]/g; + + let output: string[] = []; + basePath = normalizePath(basePath); + basePath = removeTrailingDirectorySeparator(basePath); + + let ignoreCase = !host.useCaseSensitiveFileNames; + let isExpandingRecursiveDirectory = false; + let fileSet: Map = {}; + let excludePattern: RegExp; + + // include every literal file + if (fileNames) { + for (let fileName of fileNames) { + let path = getNormalizedAbsolutePath(fileName, basePath); + includeFile(path, /*isLiteralFile*/ true); + } + } + + // expand and include the provided files into the file set. + if (includeSpecs) { + // populate the file exclusion pattern + excludePattern = createExcludeRegularExpression(basePath, excludeSpecs); + + for (let includeSpec of includeSpecs) { + includeSpec = normalizePath(includeSpec); + includeSpec = removeTrailingDirectorySeparator(includeSpec); + expandFileSpec(basePath, includeSpec, 0, includeSpec.length); + } + } + + return output; + + /** + * Expands a directory with wildcards. + * @param basePath The directory to expand. + * @param fileSpec The original file specification. + * @param start The starting offset in the file specification. + * @param end The end offset in the file specification. + */ + function expandFileSpec(basePath: string, fileSpec: string, start: number, end: number): ExpandResult { + // Skip expansion if the base path matches an exclude pattern. + if (isExcludedPath(basePath)) { + return ExpandResult.Ok; + } + + // Find the offset of the next wildcard in the file specification + let offset = indexOfWildcard(fileSpec, start); + if (offset < 0) { + // There were no more wildcards, so include the file. + let path = combinePaths(basePath, fileSpec.substring(start)); + includeFile(path, /*isLiteralFile*/ false); + return ExpandResult.Ok; + } + + // Find the last directory separator before the wildcard to get the leading path. + offset = fileSpec.lastIndexOf(directorySeparator, offset); + if (offset > start) { + // The wildcard occurs in a later segment, include remaining path up to + // wildcard in prefix. + basePath = combinePaths(basePath, fileSpec.substring(start, offset)); + + // Skip this wildcard path if the base path now matches an exclude pattern. + if (isExcludedPath(basePath)) { + return ExpandResult.Ok; } + + start = offset + 1; } - else { - var sysFiles = host.readDirectory(basePath, ".ts"); - for (var i = 0; i < sysFiles.length; i++) { - var name = sysFiles[i]; - if (!fileExtensionIs(name, ".d.ts") || !contains(sysFiles, name.substr(0, name.length - 5) + ".ts")) { - files.push(name); + + // Find the offset of the next directory separator to extract the wildcard path segment. + offset = getEndOfPathSegment(fileSpec, start, end); + + // Check if the current offset is the beginning of a recursive directory pattern. + if (isRecursiveDirectoryWildcard(fileSpec, start, offset)) { + if (offset >= end) { + // If there is no file specification following the recursive directory pattern + // we cannot match any files, so we will ignore this pattern. + return ExpandResult.Ok; + } + + // Expand the recursive directory pattern. + return expandRecursiveDirectory(basePath, fileSpec, offset + 1, end); + } + + // Match the entries in the directory against the wildcard pattern. + let pattern = createRegularExpressionFromWildcard(fileSpec, start, offset); + let entries = host.readDirectoryFlat(basePath); + for (let entry of entries) { + // Skip the entry if it does not match the pattern. + if (!pattern.test(entry)) { + continue; + } + + let path = combinePaths(basePath, entry); + if (offset >= end) { + // If the entry is a declaration file and there is a source file with the + // same name in this directory, skip the file. + if (fileExtensionIs(entry, ".d.ts") && + contains(entries, entry.substr(0, entry.length - 5) + ".ts")) { + continue; } + + // This wildcard has no further directory to process, so include the file. + includeFile(path, /*isLiteralFile*/ false); } + else if (host.directoryExists(path)) { + // If this was a directory, process the directory. + if (expandFileSpec(path, fileSpec, offset + 1, end) === ExpandResult.Error) { + return ExpandResult.Error; + } + } + } + + return ExpandResult.Ok; + } + + /** + * Expands a `**` recursive directory wildcard. + * @param basePath The directory to recursively expand. + * @param fileSpec The original file specification. + * @param start The starting offset in the file specification. + * @param end The end offset in the file specification. + */ + function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number, end: number): ExpandResult { + if (isExpandingRecursiveDirectory) { + if (errors) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)) + } + return ExpandResult.Error; } - return files; + + // expand the non-recursive part of the file specification against the prefix path. + isExpandingRecursiveDirectory = true; + let result = expandFileSpec(basePath, fileSpec, start, end); + isExpandingRecursiveDirectory = false; + + if (result !== ExpandResult.Error) { + // Recursively expand each subdirectory. + let entries = host.readDirectoryFlat(basePath); + for (let entry of entries) { + let path = combinePaths(basePath, entry); + if (host.directoryExists(path)) { + if (expandRecursiveDirectory(path, fileSpec, start, end) === ExpandResult.Error) { + result = ExpandResult.Error; + break; + } + } + } + } + + return result; + } + + /** + * Includes a file in a file set. + * @param file The file to include. + * @param isLiteralFile A value indicating whether the file to be added is + * a literal file, or the result of matching a file specification. + */ + function includeFile(file: string, isLiteralFile: boolean): void { + if (!isLiteralFile) { + // Ignore the file if it should be excluded + if (isExcludedPath(file)) { + return; + } + + // Ignore the file if it doesn't exist. + if (!host.fileExists(file)) { + return; + } + + // Ignore the file if it does not have a supported extension. + if (!options.allowNonTsExtensions && !hasSupportedFileExtension(file)) { + return; + } + } + + // Ignore the file if we've already included it. + let key = ignoreCase ? file.toLowerCase() : file; + if (hasProperty(fileSet, key)) { + return; + } + + fileSet[key] = file; + output.push(file); + } + + /** + * Determines whether a path should be excluded. + */ + function isExcludedPath(path: string) { + return excludePattern ? excludePattern.test(path) : false; + } + + /** + * Creates a regular expression from a glob-style wildcard. + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + * @param end The end offset in the file specification. + */ + function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number): RegExp { + let pattern = createPatternFromWildcard(fileSpec, start, end); + return new RegExp("^" + pattern + "$", ignoreCase ? "i" : ""); + } + + /** + * Creates a pattern from a wildcard segment + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + * @param end The end offset in the file specification. + */ + function createPatternFromWildcard(fileSpec: string, start: number, end: number): string { + let pattern = ""; + let offset = indexOfWildcard(fileSpec, start); + while (offset >= 0 && offset < end) { + if (offset > start) { + // Escape and append the non-wildcard portion to the regular expression + pattern += escapeRegularExpressionText(fileSpec, start, offset); + } + + let charCode = fileSpec.charCodeAt(offset); + if (charCode === CharacterCodes.asterisk) { + // Append a multi-character (zero or more characters) pattern to the regular expression + pattern += "[^/]*"; + } + else if (charCode === CharacterCodes.question) { + // Append a single-character (one character) pattern to the regular expression + pattern += "[^/]"; + } + else if (charCode === CharacterCodes.openBracket) { + // Append a character range (one character) pattern to the regular expression + pattern += "(?!/)["; + + // If the next character is an exclamation token, append a caret (^) to negate the + // character range and advance the start of the range by one. + start = offset + 1; + charCode = fileSpec.charCodeAt(start); + if (charCode === CharacterCodes.exclamation) { + pattern += "^"; + start++; + } + + // Find the end of the character range. If it can't be found, fix up the range + // to the end of the wildcard + offset = fileSpec.indexOf(']', start); + if (offset < 0 || offset > end) { + offset = end; + } + + // Escape and append the character range + pattern += escapeRegularExpressionText(fileSpec, start, offset); + pattern += "]"; + } + + start = offset + 1; + offset = indexOfWildcard(fileSpec, start); + } + + // Escape and append any remaining non-wildcard portion. + if (start < end) { + pattern += escapeRegularExpressionText(fileSpec, start, end); + } + + return pattern; + } + + /** + * Creates a regular expression from a glob-style wildcard used to exclude a file. + * @param basePath The prefix path + * @param excludeSpecs The file specifications to exclude. + */ + function createExcludeRegularExpression(basePath: string, excludeSpecs: string[]): RegExp { + // Ignore an empty exclusion list + if (!excludeSpecs || excludeSpecs.length === 0) { + return undefined; + } + + basePath = escapeRegularExpressionText(basePath, 0, basePath.length); + + let pattern = ""; + for (let excludeSpec of excludeSpecs) { + let excludePattern = createExcludePattern(basePath, excludeSpec); + if (excludePattern) { + if (pattern) { + pattern += "|"; + } + + pattern += "(" + excludePattern + ")"; + } + } + + if (pattern) { + return new RegExp("^(" + pattern + ")($|/)", ignoreCase ? "i" : ""); + } + + return undefined; + } + + /** + * Creates a pattern for used to exclude a file. + * @param excludeSpec The file specification to exclude. + */ + function createExcludePattern(basePath: string, excludeSpec: string): string { + if (!excludeSpec) { + return undefined; + } + + excludeSpec = normalizePath(excludeSpec); + excludeSpec = removeTrailingDirectorySeparator(excludeSpec); + + let pattern = isRootedDiskPath(excludeSpec) ? "" : basePath; + let hasRecursiveDirectoryWildcard = false; + let start = 0; + let end = excludeSpec.length; + let offset = getEndOfPathSegment(excludeSpec, start, end); + while (start < offset) { + if (isRecursiveDirectoryWildcard(excludeSpec, start, offset)) { + if (hasRecursiveDirectoryWildcard) { + if (errors) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec)); + } + return undefined; + } + + // As an optimization, if the recursive directory is the last + // wildcard, or is followed by only `*` or `*.ts`, don't add the + // remaining pattern and exit the loop. + if (canElideRecursiveDirectorySegment(excludeSpec, offset, end)) { + break; + } + + hasRecursiveDirectoryWildcard = true; + pattern += "(/.+)?";; + } + else { + if (pattern) { + pattern += directorySeparator; + } + + pattern += createPatternFromWildcard(excludeSpec, start, offset); + } + + start = offset + 1; + offset = getEndOfPathSegment(excludeSpec, start, end); + } + + return pattern; + } + + function canElideRecursiveDirectorySegment(excludeSpec: string, offset: number, end: number) { + if (offset === end || offset + 1 === end) { + return true; + } + + return canElideWildcardSegment(excludeSpec, offset + 1, end); + } + + function canElideWildcardSegment(excludeSpec: string, start: number, end: number) { + let charCode = excludeSpec.charCodeAt(start); + if (charCode === CharacterCodes.asterisk) { + if (start + 1 === end) { + return true; + } + + if (start + 4 === end && + !options.allowNonTsExtensions && + fileExtensionIs(excludeSpec, ".ts")) { + return true; + } + } + return false; + } + + /** + * Escape regular expression reserved tokens. + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + * @param end The ending offset in the file specification. + */ + function escapeRegularExpressionText(text: string, start: number, end: number) { + return text.substring(start, end).replace(reservedCharacterPattern, "\\$&"); + } + + /** + * Determines whether the wildcard at the current offset is a recursive directory wildcard. + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + * @param end The ending offset in the file specification. + */ + function isRecursiveDirectoryWildcard(fileSpec: string, start: number, end: number) { + return end - start === 2 && + fileSpec.charCodeAt(start) === CharacterCodes.asterisk && + fileSpec.charCodeAt(start + 1) === CharacterCodes.asterisk; + } + + + /** + * Gets the index of the next wildcard character in a file specification. + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + */ + function indexOfWildcard(fileSpec: string, start: number): number { + wildcardCharacterPattern.lastIndex = start; + wildcardCharacterPattern.test(fileSpec); + return wildcardCharacterPattern.lastIndex - 1; + } + + function getEndOfPathSegment(fileSpec: string, start: number, end: number): number { + if (start >= end) { + return end; + } + + let offset = fileSpec.indexOf(directorySeparator, start); + return offset < 0 || offset > end ? end : offset; } } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index ef7c892be9d31..ae3032b528a0a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -380,6 +380,19 @@ module ts { if (b === undefined) return Comparison.GreaterThan; return a < b ? Comparison.LessThan : Comparison.GreaterThan; } + + export function compareStrings(a: string, b: string, ignoreCase?: boolean) { + if (a === b) return Comparison.EqualTo; + if (a === undefined) return Comparison.LessThan; + if (b === undefined) return Comparison.GreaterThan; + if (ignoreCase) { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a === b) return Comparison.EqualTo; + } + + return a < b ? Comparison.LessThan : Comparison.GreaterThan; + } function getDiagnosticFileName(diagnostic: Diagnostic): string { return diagnostic.file ? diagnostic.file.fileName : undefined; @@ -643,7 +656,48 @@ module ts { if (path1.charAt(path1.length - 1) === directorySeparator) return path1 + path2; return path1 + directorySeparator + path2; } + + /** + * Removes a trailing directory separator from a path. + * @param path The path. + */ + export function removeTrailingDirectorySeparator(path: string) { + if (path.charAt(path.length - 1) === directorySeparator) { + return path.substr(0, path.length - 1); + } + + return path; + } + + /** + * Adds a trailing directory separator to a path, if it does not already have one. + * @param path The path. + */ + export function ensureTrailingDirectorySeparator(path: string) { + if (path.charAt(path.length - 1) !== directorySeparator) { + return path + directorySeparator; + } + + return path; + } + + export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean) { + if (a === b) return Comparison.EqualTo; + if (a === undefined) return Comparison.LessThan; + if (b === undefined) return Comparison.GreaterThan; + a = removeTrailingDirectorySeparator(a); + b = removeTrailingDirectorySeparator(b); + let aComponents = getNormalizedPathComponents(a, currentDirectory); + let bComponents = getNormalizedPathComponents(b, currentDirectory); + let sharedLength = Math.min(aComponents.length, bComponents.length); + for (let i = 0; i < sharedLength; ++i) { + let result = compareStrings(aComponents[i], bComponents[i], ignoreCase); + if (result) return result; + } + return compareValues(aComponents.length, bComponents.length); + } + export function fileExtensionIs(path: string, extension: string): boolean { let pathLen = path.length; let extLen = extension.length; @@ -654,6 +708,10 @@ module ts { * List of supported extensions in order of file resolution precedence. */ export const supportedExtensions = [".ts", ".d.ts"]; + + export function hasSupportedFileExtension(file: string) { + return forEach(supportedExtensions, extension => fileExtensionIs(file, extension)); + } const extensionsToRemove = [".d.ts", ".ts", ".js"]; export function removeFileExtension(path: string): string { diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index 2a8527168464a..8fd58e4fe4d3c 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -438,6 +438,7 @@ module ts { Loop_contains_block_scoped_variable_0_referenced_by_a_function_in_the_loop_This_is_only_supported_in_ECMAScript_6_or_higher: { code: 4091, category: DiagnosticCategory.Error, key: "Loop contains block-scoped variable '{0}' referenced by a function in the loop. This is only supported in ECMAScript 6 or higher." }, The_current_host_does_not_support_the_0_option: { code: 5001, category: DiagnosticCategory.Error, key: "The current host does not support the '{0}' option." }, Cannot_find_the_common_subdirectory_path_for_the_input_files: { code: 5009, category: DiagnosticCategory.Error, key: "Cannot find the common subdirectory path for the input files." }, + File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0: { code: 5011, category: DiagnosticCategory.Error, key: "File specification cannot contain multiple recursive directory wildcards ('**'): '{0}'." }, Cannot_read_file_0_Colon_1: { code: 5012, category: DiagnosticCategory.Error, key: "Cannot read file '{0}': {1}" }, Unsupported_file_encoding: { code: 5013, category: DiagnosticCategory.Error, key: "Unsupported file encoding." }, Failed_to_parse_file_0_Colon_1: { code: 5014, category: DiagnosticCategory.Error, key: "Failed to parse file '{0}': {1}." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cf1645b140724..a5459e974bcfc 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1741,6 +1741,10 @@ "category": "Error", "code": 5009 }, + "File specification cannot contain multiple recursive directory wildcards ('**'): '{0}'.": { + "category": "Error", + "code": 5011 + }, "Cannot read file '{0}': {1}": { "category": "Error", "code": 5012 diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index f9daf52c5f28d..87948cf6aa869 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -16,6 +16,7 @@ module ts { getExecutingFilePath(): string; getCurrentDirectory(): string; readDirectory(path: string, extension?: string): string[]; + readDirectoryFlat(path: string): string[]; getMemoryUsage?(): number; exit(exitCode?: number): void; } @@ -135,6 +136,11 @@ module ts { } } } + + function readDirectoryFlat(path: string): string[] { + var folder = fso.GetFolder(path || "."); + return [...getNames(folder.files), ...getNames(folder.subfolders)]; + } return { args, @@ -166,6 +172,7 @@ module ts { return new ActiveXObject("WScript.Shell").CurrentDirectory; }, readDirectory, + readDirectoryFlat, exit(exitCode?: number): void { try { WScript.Quit(exitCode); @@ -246,6 +253,10 @@ module ts { } } } + + function readDirectoryFlat(path: string): string[] { + return _fs.readdirSync(path || ".").sort(); + } return { args: process.argv.slice(2), @@ -277,7 +288,7 @@ module ts { return _path.resolve(path); }, fileExists(path: string): boolean { - return _fs.existsSync(path); + return _fs.existsSync(path) && _fs.statSync(path).isFile(); }, directoryExists(path: string) { return _fs.existsSync(path) && _fs.statSync(path).isDirectory(); @@ -294,6 +305,7 @@ module ts { return process.cwd(); }, readDirectory, + readDirectoryFlat, getMemoryUsage() { if (global.gc) { global.gc(); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 15cc665e35177..24a9283482573 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -242,7 +242,7 @@ module ts { setCachedProgram(compileResult.program); reportDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } - + function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError ?: (message: string) => void) { // Return existing SourceFile object if one is available if (cachedProgram) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 86e680ca8b047..98ad1f3588b9a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1027,7 +1027,7 @@ module ts { // This field should never be used directly to obtain line map, use getLineMap function instead. /* @internal */ lineMap: number[]; } - + export interface ScriptReferenceHost { getCompilerOptions(): CompilerOptions; getSourceFile(fileName: string): SourceFile; @@ -1035,7 +1035,27 @@ module ts { } export interface ParseConfigHost { + useCaseSensitiveFileNames: boolean; + readDirectory(rootDir: string, extension: string): string[]; + + /** + * Gets a value indicating whether the specified path exists. + * @param path The path to test. + */ + fileExists(path: string): boolean; + + /** + * Gets a value indicating whether the specified path exists and is a directory. + * @param path The path to test. + */ + directoryExists(path: string): boolean; + + /** + * Reads the files in the directory. + * @param path The directory path. + */ + readDirectoryFlat(path: string): string[]; } export interface WriteFileCallback { diff --git a/src/harness/external/chai.d.ts b/src/harness/external/chai.d.ts index 814de75e7b254..2fa5b20de476c 100644 --- a/src/harness/external/chai.d.ts +++ b/src/harness/external/chai.d.ts @@ -171,5 +171,8 @@ declare module chai { function isFalse(value: any, message?: string): void; function isNull(value: any, message?: string): void; function isNotNull(value: any, message?: string): void; + function deepEqual(actual: any, expected: any, message?: string): void; + function notDeepEqual(actual: any, expected: any, message?: string): void; + function lengthOf(object: any[], length: number, message?: string): void; } } \ No newline at end of file diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 0e0b8d829183d..2c87f2e6b93f1 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -232,6 +232,10 @@ module Harness.LanguageService { readDirectory(rootDir: string, extension: string): string { throw new Error("NYI"); } + + readDirectoryFlat(path: string): string { + throw new Error("NYI"); + } log(s: string): void { this.nativeHost.log(s); } trace(s: string): void { this.nativeHost.trace(s); } @@ -537,6 +541,10 @@ module Harness.LanguageService { throw new Error("Not implemented Yet."); } + readDirectoryFlat(path: string): string[] { + throw new Error("Not implemented Yet."); + } + watchFile(fileName: string, callback: (fileName: string) => void): ts.FileWatcher { return { close() { } }; } diff --git a/src/harness/runner.ts b/src/harness/runner.ts index 37451639d75fc..a5308e8546e21 100644 --- a/src/harness/runner.ts +++ b/src/harness/runner.ts @@ -37,11 +37,19 @@ var testConfigFile = Harness.IO.fileExists(mytestconfig) ? Harness.IO.readFile(mytestconfig) : (Harness.IO.fileExists(testconfig) ? Harness.IO.readFile(testconfig) : ''); +var unitTestsRequested = false; + if (testConfigFile !== '') { - // TODO: not sure why this is crashing mocha - //var testConfig = JSON.parse(testConfigRaw); - var testConfig = testConfigFile.match(/test:\s\['(.*)'\]/); - var options = testConfig ? [testConfig[1]] : []; + var options: string[]; + try { + let testConfigJson = JSON.parse(testConfigFile); + options = testConfigJson.test instanceof Array ? testConfigJson.test : []; + } + catch (e) { + let testConfig = testConfigFile.match(/test:\s\['(.*)'\]/); + options = testConfig ? [testConfig[1]] : []; + } + for (var i = 0; i < options.length; i++) { switch (options[i]) { case 'compiler': @@ -73,11 +81,14 @@ if (testConfigFile !== '') { case 'test262': runners.push(new Test262BaselineRunner()); break; + case 'unit': + unitTestsRequested = true; + break; } } } -if (runners.length === 0) { +if (runners.length === 0 && !unitTestsRequested) { // compiler runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); diff --git a/src/services/shims.ts b/src/services/shims.ts index 27c70ddc878a4..7de930cce319a 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -62,6 +62,10 @@ module ts { export interface CoreServicesShimHost extends Logger { /** Returns a JSON-encoded value of the type: string[] */ readDirectory(rootDir: string, extension: string): string; + readDirectoryFlat?(path: string): string; + useCaseSensitiveFileNames?: boolean; + fileExists?(path: string): boolean; + directoryExists?(path: string): boolean; } /// @@ -335,13 +339,53 @@ module ts { export class CoreServicesShimHostAdapter implements ParseConfigHost { + public useCaseSensitiveFileNames: boolean; + constructor(private shimHost: CoreServicesShimHost) { + if (typeof shimHost.useCaseSensitiveFileNames === "boolean") { + this.useCaseSensitiveFileNames = shimHost.useCaseSensitiveFileNames; + } + else if (sys) { + this.useCaseSensitiveFileNames = sys.useCaseSensitiveFileNames; + } + else { + this.useCaseSensitiveFileNames = true; + } } - + public readDirectory(rootDir: string, extension: string): string[] { var encoded = this.shimHost.readDirectory(rootDir, extension); return JSON.parse(encoded); } + + public readDirectoryFlat(path: string): string[] { + path = normalizePath(path); + path = ensureTrailingDirectorySeparator(path); + if (this.shimHost.readDirectoryFlat) { + var encoded = this.shimHost.readDirectoryFlat(path); + return JSON.parse(encoded); + } + + return sys ? sys.readDirectoryFlat(path): []; + } + + public fileExists(path: string): boolean { + path = normalizePath(path); + if (this.shimHost.fileExists) { + return this.shimHost.fileExists(path); + } + + return sys ? sys.fileExists(path) : undefined; + } + + public directoryExists(path: string): boolean { + path = normalizePath(path); + if (this.shimHost.directoryExists) { + return this.shimHost.directoryExists(path); + } + + return sys ? sys.directoryExists(path) : undefined; + } } function simpleForwardCall(logger: Logger, actionDescription: string, action: () => any, noPerfLogging: boolean): any { diff --git a/tests/cases/unittests/expandFiles.ts b/tests/cases/unittests/expandFiles.ts new file mode 100644 index 0000000000000..22b85687e9c48 --- /dev/null +++ b/tests/cases/unittests/expandFiles.ts @@ -0,0 +1,236 @@ +/// +/// + +describe("expandFiles", () => { + const basePath = "c:/dev/"; + const caseInsensitiveHost = createMockParseConfigHost( + basePath, + /*files*/ [ + "c:/dev/a.ts", + "c:/dev/a.d.ts", + "c:/dev/a.js", + "c:/dev/b.ts", + "c:/dev/b.js", + "c:/dev/c.d.ts", + "c:/dev/z/a.ts", + "c:/dev/z/abz.ts", + "c:/dev/z/aba.ts", + "c:/dev/z/b.ts", + "c:/dev/z/bbz.ts", + "c:/dev/z/bba.ts", + "c:/dev/x/a.ts", + "c:/dev/x/aa.ts", + "c:/dev/x/b.ts", + "c:/dev/x/y/a.ts", + "c:/dev/x/y/b.ts" + ], + /*ignoreCase*/ true); + + const caseSensitiveHost = createMockParseConfigHost( + basePath, + /*files*/ [ + "c:/dev/a.ts", + "c:/dev/a.d.ts", + "c:/dev/a.js", + "c:/dev/b.ts", + "c:/dev/b.js", + "c:/dev/A.ts", + "c:/dev/B.ts", + "c:/dev/c.d.ts", + "c:/dev/z/a.ts", + "c:/dev/z/abz.ts", + "c:/dev/z/aba.ts", + "c:/dev/z/b.ts", + "c:/dev/z/bbz.ts", + "c:/dev/z/bba.ts", + "c:/dev/x/a.ts", + "c:/dev/x/b.ts", + "c:/dev/x/y/a.ts", + "c:/dev/x/y/b.ts", + ], + /*ignoreCase*/ false); + + let expect = _chai.expect; + describe("with literal file list", () => { + it("without exclusions", () => { + let fileNames = ["a.ts", "b.ts"]; + let results = ts.expandFiles(basePath, fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]); + }); + it("missing files are still present", () => { + let fileNames = ["z.ts", "x.ts"]; + let results = ts.expandFiles(basePath, fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/z.ts", "c:/dev/x.ts"]); + }); + it("are not removed due to excludes", () => { + let fileNames = ["a.ts", "b.ts"]; + let excludeSpecs = ["b.ts"]; + let results = ts.expandFiles(basePath, fileNames, /*includeSpecs*/ undefined, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]); + }); + }); + + describe("with literal include list", () => { + it("without exclusions", () => { + let includeSpecs = ["a.ts", "b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]); + }); + it("with non .ts file extensions are excluded", () => { + let includeSpecs = ["a.js", "b.js"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, []); + }); + it("with missing files are excluded", () => { + let includeSpecs = ["z.ts", "x.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, []); + }); + it("with literal excludes", () => { + let includeSpecs = ["a.ts", "b.ts"]; + let excludeSpecs = ["b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts"]); + }); + it("with wildcard excludes", () => { + let includeSpecs = ["a.ts", "b.ts", "z/a.ts", "z/abz.ts", "z/aba.ts", "x/b.ts"]; + let excludeSpecs = ["*.ts", "z/??[b-z].ts", "*/b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/z/a.ts", "c:/dev/z/aba.ts"]); + }); + it("with recursive excludes", () => { + let includeSpecs = ["a.ts", "b.ts", "x/a.ts", "x/b.ts", "x/y/a.ts", "x/y/b.ts"]; + let excludeSpecs = ["**/b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts"]); + }); + it("with case sensitive exclude", () => { + let includeSpecs = ["B.ts"]; + let excludeSpecs = ["**/b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseSensitiveHost); + assert.deepEqual(results, ["c:/dev/B.ts"]); + }); + }); + + describe("with wildcard include list", () => { + it("same named declarations are excluded", () => { + let includeSpecs = ["*.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"]); + }); + it("`*` matches only ts files", () => { + let includeSpecs = ["*"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"]); + }); + it("`?` matches only a single character", () => { + let includeSpecs = ["x/?.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/x/a.ts", "c:/dev/x/b.ts"]); + }); + it("`[]` matches only a single character", () => { + let includeSpecs = ["x/[b-z].ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/x/b.ts"]); + }); + it("with recursive directory", () => { + let includeSpecs = ["**/a.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts", "c:/dev/z/a.ts"]); + }); + it("case sensitive", () => { + let includeSpecs = ["**/A.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseSensitiveHost); + assert.deepEqual(results, ["c:/dev/A.ts"]); + }); + it("with missing files are excluded", () => { + let includeSpecs = ["*/z.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, []); + }); + it("always include literal files", () => { + let fileNames = ["a.ts"]; + let includeSpecs = ["*/z.ts"]; + let excludeSpecs = ["**/a.ts"]; + let results = ts.expandFiles(basePath, fileNames, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts"]); + }); + }); + + function createMockParseConfigHost(basePath: string, files: string[], ignoreCase: boolean): ts.ParseConfigHost { + let fileSet: ts.Map = {}; + let directorySet: ts.Map> = {}; + + files.sort((a, b) => ts.comparePaths(a, b, basePath, ignoreCase)); + for (let file of files) { + addFile(ts.getNormalizedAbsolutePath(file, basePath)); + } + + return { + useCaseSensitiveFileNames: !ignoreCase, + fileExists, + directoryExists, + readDirectory, + readDirectoryFlat + }; + + function fileExists(path: string): boolean { + let fileKey = ignoreCase ? path.toLowerCase() : path; + return ts.hasProperty(fileSet, fileKey); + } + + function directoryExists(path: string): boolean { + let directoryKey = ignoreCase ? path.toLowerCase() : path; + return ts.hasProperty(directorySet, directoryKey); + } + + function readDirectory(rootDir: string, extension: string): string[] { + throw new Error("Not implemented"); + } + + function readDirectoryFlat(path: string): string[] { + path = ts.getNormalizedAbsolutePath(path, basePath); + path = ts.removeTrailingDirectorySeparator(path); + let directoryKey = ignoreCase ? path.toLowerCase() : path; + let entries = ts.getProperty(directorySet, directoryKey); + let result: string[] = []; + ts.forEachKey(entries, key => { result.push(key); }); + result.sort((a, b) => ts.compareStrings(a, b, ignoreCase)); + return result; + } + + function addFile(file: string) { + let fileKey = ignoreCase ? file.toLowerCase() : file; + if (!ts.hasProperty(fileSet, fileKey)) { + fileSet[fileKey] = file; + let name = ts.getBaseFileName(file); + let parent = ts.getDirectoryPath(file); + addToDirectory(parent, name); + } + } + + function addDirectory(directory: string) { + directory = ts.removeTrailingDirectorySeparator(directory); + let directoryKey = ignoreCase ? directory.toLowerCase() : directory; + if (!ts.hasProperty(directorySet, directoryKey)) { + directorySet[directoryKey] = {}; + let name = ts.getBaseFileName(directory); + let parent = ts.getDirectoryPath(directory); + if (parent !== directory) { + addToDirectory(parent, name); + } + } + } + + function addToDirectory(directory: string, entry: string) { + addDirectory(directory); + directory = ts.removeTrailingDirectorySeparator(directory); + let directoryKey = ignoreCase ? directory.toLowerCase() : directory; + let entryKey = ignoreCase ? entry.toLowerCase() : entry; + let entries = directorySet[directoryKey]; + if (!ts.hasProperty(entries, entryKey)) { + entries[entryKey] = entry; + } + } + } +});