├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── package-lock.json ├── package.json ├── src ├── cli.ts ├── index.ts ├── ts.ts └── utilities.ts ├── test ├── integration │ ├── __snapshots__ │ │ ├── addMissingMember.shot │ │ ├── addMissingOverride.shot │ │ ├── addMissingOverrideByError.shot │ │ ├── addMissingOverrideByFixName.shot │ │ ├── addOneUnknown.shot │ │ ├── addOneUnknownToList.shot │ │ ├── addThreeUnknown.shot │ │ ├── multipleFixesInSameLine.shot │ │ ├── noDiagnostics.shot │ │ ├── twoErrorCodes.shot │ │ └── twoFixNames.shot │ ├── cases │ │ ├── addMissingMember │ │ │ ├── a.ts │ │ │ ├── b.ts │ │ │ ├── cmd.txt │ │ │ ├── tsconfig.json │ │ │ └── types.ts │ │ ├── addMissingOverride │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── addMissingOverrideByError │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── addMissingOverrideByFixName │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── addOneUnknown │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── addOneUnknownToList │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── addThreeUnknown │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── multipleFixesInSameLine │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── noDiagnostics │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ ├── twoErrorCodes │ │ │ ├── addoverrides.ts │ │ │ ├── addunknowns.ts │ │ │ ├── cmd.txt │ │ │ └── tsconfig.json │ │ └── twoFixNames │ │ │ ├── addoverrides.ts │ │ │ ├── addunknowns.ts │ │ │ ├── cmd.txt │ │ │ └── tsconfig.json │ ├── integration.test.ts │ ├── needFixing │ │ ├── addUndefinedExactOptionalPropertyTypes.shot │ │ ├── addUndefinedExactOptionalPropertyTypes │ │ │ ├── a.ts │ │ │ ├── b.ts │ │ │ ├── cmd.txt │ │ │ ├── tsconfig.json │ │ │ └── types.ts │ │ ├── noWriteUndefinedExactOptionalPropertyTypes.shot │ │ ├── noWriteUndefinedExactOptionalPropertyTypes │ │ │ ├── a.ts │ │ │ ├── b.ts │ │ │ ├── cmd.txt │ │ │ ├── tsconfig.json │ │ │ └── types.ts │ │ ├── noWriteaddMissingOverride.shot │ │ └── noWriteaddMissingOverride │ │ │ ├── cmd.txt │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ └── testHost.ts ├── tsconfig.json └── unit │ ├── checkOptions.test.ts │ ├── doTextChangeOnString.test.ts │ ├── filterCodefixesByName.test.ts │ ├── filterDiagnosticsByFileAndErrorCode.test.ts │ ├── getAllNoAppliedChangesByFile.test.ts │ ├── getTextChangeDict.test.ts │ ├── makeOptions.test.ts │ ├── outputFileName.test.ts │ ├── pathOperations.test.ts │ ├── removeDuplicatedFixes.test.ts │ └── removeMultipleDiagnostics.test.ts ├── tsconfig.json ├── tsdxstart.md └── vitest.config.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 15 | - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 16 | with: 17 | node-version: '*' 18 | cache: 'npm' 19 | - run: npm ci 20 | - run: npm run build --if-present 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | exampleFolder 4 | presrc 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | // See: https://github.com/microsoft/TypeScript/wiki/Debugging-Language-Service-in-VS-Code 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attach to VS Code TS Server via Port", 12 | "processId": "${command:PickProcess}", 13 | // "customDescriptionGenerator": "'__tsDebuggerDisplay' in this ? this.__tsDebuggerDisplay(defaultValue) : defaultValue", 14 | } 15 | 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Isabel Duan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This tool is to automate the application of TypeScript codefixes across your TypeScript repositories. 3 | 4 | 5 | # Download 6 | If cloning from GitHub, after cloning, run 7 | ``` 8 | npm install 9 | npm run build 10 | npm link 11 | ``` 12 | `ts-fix` can then be used in the command line 13 | 14 | 15 | # Example Usage 16 | ``` 17 | ts-fix -t path/to/tsconfig.json -f nameOfCodefix 18 | ``` 19 | 20 | ``` 21 | ts-fix -e 4114 --write 22 | ``` 23 | 24 | ``` 25 | ts-fix --interactiveMode --file relativePathToFile 26 | ``` 27 | 28 | # Flags 29 | 30 | ``` 31 | Options: 32 | --help Show help [boolean] 33 | --version Show version number [boolean] 34 | -t, --tsconfig Path to project's tsconfig 35 | [string] [default: "./tsconfig.json"] 36 | -e, --errorCode The error code(s) [number] [default: []] 37 | -f, --fixName The name(s) of codefixe(s) to apply [string] [default: []] 38 | -w, --write Tool will only emit or overwrite files if --write is included [boolean] [default: false] 39 | -o, --outputFolder Path of output directory [string] 40 | --interactiveMode Enables interactive mode [boolean] [default: false] 41 | --file Relative paths to files [string] [default: []] 42 | --showMultiple Shows multiple fixes for a diagnostic 43 | [boolean] [default: false] 44 | --ignoreGitStatus Ignore working tree and force the emitting or overwriting of files [boolean] [default: false] 45 | ``` 46 | 47 | `-t path/to/tsconfig.json` or `--tsconfig path/to/tsconfig.json` 48 | Specifies the project to use the tool on. If no argument given, the tool will use the tsconfig in the current working directory. 49 | 50 | `-e ` or `--errorCode ` 51 | Specifies the errors to fix. Several error codes can be specified during the same command. 52 | The easiest way to find error codes in your repository is to hover over the error in your IDE. 53 | 54 | `-f ` or `--fixName ` 55 | Specifies the types of codefixes to use. Several names can be specified. 56 | 57 | If both error numbers and fix names are given, then in order to be applied, a fix must be generated by a specified error and have a name that was specified. 58 | 59 | `--write` 60 | Boolean for if the tool should overwrite previous code files with the codefixed code or emit any files with codefixed code. If `--write` not included, then the tool will print to console which files would have changed. 61 | 62 | `--interactiveMode` 63 | Boolean for enabling interactive CLI to visualize which code fixes to apply to your project. Some special cases to keep in mind are: 64 | 1. One diagnostic might be tied to more than one code fix. A simple example of this is when the paramenters of a functions have any type and the `inferFromUsage` fix is recommended. Let's say that you want to fix only error code `7019`, this could also fix `7006` if the function parameters has both diagnostics. 65 | 2. Some codefixes are applied on a different location from where the actual diagnostic is.Some examples: 66 | 2.1 When the code fix is applied on the diagnostic's related information instead. 67 | 2.2 When the code fix is applied in an entire different file from the original file. 68 | 69 | `--file ` 70 | Relative file path(s) to the file(s) in which to find diagnostics and apply quick fixes. The path is relative to the project folder. 71 | 72 | `--showMultiple` 73 | Boolean for enabling showing multiple fixes for diagnostics for which this applies. 74 | One consideration when `--showMultiple = true` is that the tool migth not be able to find consecutives fixes afecting the same span if those diagnostics have mutliple fixes. 75 | 76 | `--ignoreGitStatus` 77 | Boolean to force the overwriting of files when `--write = true` and output folder matches project folder. If you are sure you would like to run ts-fix on top of your current changes provide this flag. 78 | 79 | 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "bin": "dist/cli.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "engines": { 12 | "node": ">=14.17" 13 | }, 14 | "scripts": { 15 | "start": "tsc -p . --watch", 16 | "build": "tsc -p . && tsc -p test", 17 | "test": "vitest run", 18 | "prepare": "tsc -p ." 19 | }, 20 | "name": "ts-fix", 21 | "private": true, 22 | "author": "Isabel Duan", 23 | "devDependencies": { 24 | "@types/diff": "^7.0.1", 25 | "@types/inquirer": "^8.2.10", 26 | "@types/lodash": "^4.17.16", 27 | "@types/yargs": "^17.0.33", 28 | "vitest": "^3.0.7" 29 | }, 30 | "dependencies": { 31 | "diff": "^7.0.0", 32 | "import-cwd": "^3.0.0", 33 | "inquirer": "^8.2.6", 34 | "lodash": "^4.17.21", 35 | "typescript": "^5.5.3", 36 | "yargs": "^17.7.2" 37 | }, 38 | "packageManager": "npm@8.19.4+sha256.2667a1b8300f315d223e43c307fbe946eb8b97792af399424ef67ea9cb0a72f6" 39 | } 40 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import yargs from 'yargs'; 4 | import path from "path"; 5 | import { Options, codefixProject, CLIHost } from '.'; 6 | 7 | export function makeOptions(cwd: string, args: string[]): Options { 8 | const { 9 | errorCode, 10 | file, 11 | fixName, 12 | ignoreGitStatus, 13 | interactiveMode, 14 | outputFolder, 15 | showMultiple, 16 | tsconfig, 17 | write, 18 | } = yargs(args) 19 | .scriptName("ts-fix") 20 | .usage("$0 -t path/to/tsconfig.json") 21 | .option("errorCode", { 22 | alias: "e", 23 | describe: "The error code(s)", 24 | type: "number", 25 | array: true, 26 | default: [] 27 | }) 28 | .option("file", { 29 | description: "Relative paths to the file(s) for which to find diagnostics", 30 | type: "string", 31 | array: true, 32 | default: [] 33 | }) 34 | .option("fixName", { 35 | alias: "f", 36 | describe: "The name(s) of codefixe(s) to apply", 37 | type: "string", 38 | array: true, 39 | default: [] 40 | }) 41 | .option("ignoreGitStatus", { 42 | describe: "Must use if the git status isn't clean, the write flag is being used, and the output folder is the same as the project folder", 43 | type: "boolean", 44 | default: false 45 | }) 46 | .option("interactiveMode", { 47 | describe: "Takes input from the user to decide which fixes to apply", 48 | type: "boolean", 49 | default: false 50 | }) 51 | .option("outputFolder", { 52 | alias: "o", 53 | describe: "Path of output directory", 54 | type: "string" 55 | }) 56 | .option("showMultiple", { 57 | describe: "Takes input from the user to decide which fix to apply when there are more than one quick fix for a diagnostic, if this flag is not provided the tool will apply the first fix found for the diagnostic", 58 | type: "boolean", 59 | default: false 60 | }) 61 | .option("tsconfig", { 62 | alias: "t", 63 | description: "Path to project's tsconfig", 64 | type: "string", 65 | nargs: 1, 66 | default: "./tsconfig.json", 67 | coerce: (arg: string) => { 68 | return path.resolve(cwd, arg); 69 | } 70 | }) 71 | .option("write", { 72 | alias: "w", 73 | describe: "Tool will only emit or overwrite files if --write is included.", 74 | type: "boolean", 75 | default: false 76 | }) 77 | .parseSync(); 78 | return { 79 | cwd, 80 | errorCode, 81 | file, 82 | fixName, 83 | ignoreGitStatus, 84 | interactiveMode, 85 | outputFolder : outputFolder ? path.resolve(cwd, outputFolder) : path.dirname(tsconfig), 86 | showMultiple, 87 | tsconfig, 88 | write, 89 | }; 90 | } 91 | 92 | if (!module.parent) { 93 | const opt = makeOptions(process.cwd(), process.argv.slice(2)); 94 | let host = new CLIHost(process.cwd()); 95 | (async () => { 96 | const error = await codefixProject(opt, host); 97 | host.log(error); 98 | })(); 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { CodeFixAction, Diagnostic, FileTextChanges, formatDiagnosticsWithColorAndContext, SourceFile, TextChange } from "typescript"; 3 | import os from "os"; 4 | import _, { flatMap, cloneDeep, isEqual, map } from "lodash"; 5 | import { createProject, Project } from "./ts"; 6 | import * as fs from "fs"; 7 | import { diffChars } from "diff"; 8 | import inquirer from "inquirer"; 9 | import { formatDiagnosticsWithColorAndContextTsFix, formatFixesInTheSameSpan, formatFixOnADifferentLocation } from "./utilities"; 10 | import { exec } from 'child_process'; 11 | 12 | export interface Logger { 13 | (...args: any[]): void; 14 | error?(...args: any[]): void; 15 | warn?(...args: any[]): void; 16 | info?(...args: any[]): void; 17 | verbose?(...args: any[]): void; 18 | } 19 | export interface Host { 20 | // Emits one particular file with input fileName and content string 21 | writeFile(fileName: string, content: string): void; 22 | 23 | // Returns all text changes that were not applied 24 | getRemainingChanges: () => (ReadonlyMap)[]; 25 | getNewLine(): string; 26 | 27 | // Adds map of text changes that were not applied 28 | addRemainingChanges: (changelist: ReadonlyMap) => void; 29 | log: Logger; 30 | write: Logger; 31 | mkdir: typeof import("fs").mkdirSync; 32 | exists: typeof import("fs").existsSync; 33 | getCurrentDirectory(): string; 34 | getCanonicalFileName(fileName: string): string; 35 | } 36 | 37 | export function identity(x: T) { 38 | return x; 39 | } 40 | 41 | export function toLowerCase(x: string) { 42 | return x.toLowerCase(); 43 | } 44 | 45 | const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g; 46 | export function toFileNameLowerCase(x: string) { 47 | return fileNameLowerCaseRegExp.test(x) ? 48 | x.replace(fileNameLowerCaseRegExp, toLowerCase) : 49 | x; 50 | } 51 | 52 | export type GetCanonicalFileName = (fileName: string) => string; 53 | export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { 54 | return useCaseSensitiveFileNames ? identity : toFileNameLowerCase; 55 | } 56 | export class CLIHost implements Host { 57 | 58 | private remainingChanges: (ReadonlyMap)[] = []; 59 | 60 | constructor(private cwd: string) { }; 61 | 62 | writeFile(fileName: string, content: string) { fs.writeFileSync(fileName, content, 'utf8') }; 63 | 64 | getRemainingChanges() { return this.remainingChanges }; 65 | 66 | addRemainingChanges(changeList: ReadonlyMap) { this.remainingChanges.push(changeList) }; 67 | 68 | log(s: string) { console.log(s) }; 69 | 70 | write(s: string) { process.stdout.write(s) }; 71 | 72 | mkdir(directoryPath: fs.PathLike) { return fs.mkdirSync(directoryPath, { recursive: true }) }; 73 | 74 | exists(fileName: fs.PathLike) { return fs.existsSync(fileName) }; 75 | 76 | getNewLine() { return "\r\n" } 77 | 78 | getCurrentDirectory() { return process.cwd() } 79 | 80 | getCanonicalFileName(fileName: string) { return fileName.toLowerCase() } 81 | } 82 | 83 | export interface Options { 84 | cwd: string; 85 | tsconfig: string; 86 | outputFolder: string; 87 | errorCode: number[]; 88 | file: string[]; 89 | fixName: string[]; 90 | write: boolean, 91 | showMultiple: boolean, 92 | interactiveMode: boolean, 93 | ignoreGitStatus: boolean 94 | } 95 | 96 | // Get git status from git repository 97 | // Note: Only works if the git repository folder matches tsconfig folder 98 | const getGitStatus = (dir: string) => new Promise((resolve) => { 99 | const dirPath = path.dirname(dir); 100 | return exec(`git --git-dir="${path.join(dirPath, ".git")}" --work-tree="${dirPath}" status --porcelain`, (err, stdout) => { 101 | if (err) { 102 | throw new Error(err.message); 103 | } 104 | else if (typeof stdout === 'string') { 105 | resolve(stdout.trim()); 106 | } 107 | }); 108 | }); 109 | 110 | // Check git status and paths to provided files if applicable 111 | export const checkOptions = async (opt: Options): Promise<[string[], string[]]> => { 112 | 113 | // Check git status if the write flag was provided and the output folder is the same as the project folder 114 | // Do not allow overwriting files with previous changes on them unless --ignoreGitStatus flag was provided 115 | if (!opt.ignoreGitStatus && opt.write && path.dirname(opt.tsconfig) === opt.outputFolder) { 116 | let isModified = false; 117 | const status = await (getGitStatus(opt.tsconfig)); 118 | const splitStatus = status.split(/\r?\n/); 119 | if (splitStatus.length && splitStatus[0] != '') { 120 | const re = /[MARCD?]\s(package).+?(json)/g 121 | isModified = splitStatus.length && splitStatus[0] !== '' ? !(splitStatus.filter((text) => { return text.match(re) }).length === splitStatus.length) : false; 122 | } 123 | if (isModified) { 124 | throw new Error(`Please provide the --ignoreGitStatus flag if you are sure you'd like to override your existing changes`); 125 | } 126 | } 127 | 128 | // Keep track of provided files regardless if they are valid or invalid 129 | // If all file paths are invalid throw an error 130 | let validFiles = new Array; 131 | let invalidFiles = new Array; 132 | if (opt.file.length) { 133 | opt.file.forEach((file) => { 134 | file = path.join(path.dirname(opt.tsconfig), file); 135 | if (fs.existsSync(file)) { 136 | validFiles.push(file); 137 | } 138 | else { 139 | invalidFiles.push(file); 140 | } 141 | }); 142 | if (!validFiles.length) { 143 | throw new Error(`All provided files are invalid`); 144 | } 145 | } 146 | return [validFiles, invalidFiles]; 147 | } 148 | 149 | // Print summary of the run at the end of the project 150 | function printSummary(host: Host, opt: Options, invalidFiles: string[], allChangedFiles: Map, noAppliedChangesByFile: Map>): void { 151 | if (invalidFiles.length) { 152 | host.log(`${host.getNewLine()}The following file paths are invalid:`); 153 | invalidFiles.forEach((file) => host.log(file)); 154 | } 155 | if (noAppliedChangesByFile.size) { 156 | host.log(`${host.getNewLine()}The fixes not applied by ts-fix for this project are:`); 157 | noAppliedChangesByFile.forEach((fileNames, fix) => { 158 | fileNames.forEach((fileName) => { 159 | if (fileName && fix) { 160 | host.log(`${fix} ${path.relative(opt.cwd, fileName)}`) 161 | } 162 | }); 163 | }) 164 | } 165 | if (opt.write) { 166 | const allChangedFilesSize = allChangedFiles.size; 167 | if (!allChangedFilesSize) { 168 | host.log(`${host.getNewLine()}No changes made in any files`); 169 | } 170 | else { 171 | host.log(`${host.getNewLine()}Changes were made in the following files:`); 172 | } 173 | allChangedFiles.forEach((changedFile, fileName) => { 174 | writeToFile(fileName, changedFile.newText, opt, host); 175 | }) 176 | } 177 | } 178 | 179 | // Updates allNoAppliedChangesByFile at the end of each pass to have the keep track of all no applied fixes 180 | export function getAllNoAppliedChangesByFile(allNoAppliedChangesByFile: Map>, noAppliedFixes: CodeFixAction[]): Map> { 181 | noAppliedFixes.forEach((fix) => { 182 | if (allNoAppliedChangesByFile.has(fix.fixName)) { 183 | let newSet: Set = new Set(); 184 | if (!fix.changes.length && fix.commands) { 185 | newSet = allNoAppliedChangesByFile.get(fix.fixName)!; 186 | newSet.add((fix.commands[0] as any).file); 187 | allNoAppliedChangesByFile.set(fix.fixName, newSet); 188 | } 189 | else { 190 | newSet = allNoAppliedChangesByFile.get(fix.fixName)!; 191 | newSet.add((fix.changes[0] as any).fileName); 192 | allNoAppliedChangesByFile.set(fix.fixName, newSet); 193 | } 194 | } 195 | else { 196 | let newSet: Set = new Set(); 197 | if (!fix.changes.length && fix.commands) { 198 | allNoAppliedChangesByFile.set(fix.fixName, newSet.add((fix.commands[0] as any).file)); 199 | } 200 | else { 201 | allNoAppliedChangesByFile.set(fix.fixName, newSet.add(fix.changes[0].fileName)); 202 | } 203 | } 204 | }); 205 | return allNoAppliedChangesByFile; 206 | } 207 | 208 | export async function codefixProject(opt: Options, host: Host): Promise { 209 | 210 | const [validFiles, invalidFiles] = await checkOptions(opt); 211 | 212 | const allChangedFiles = new Map(); 213 | let allNoAppliedChangesByFile = new Map>; 214 | let passCount = 0; 215 | 216 | while (true) { 217 | 218 | if (passCount === 0) { 219 | host.log(`The project is being created...${host.getNewLine()}`); 220 | } 221 | 222 | const project = createProject({ tsConfigFilePath: opt.tsconfig }, allChangedFiles); 223 | if (!project) { 224 | return "Error: Could not create project."; 225 | } 226 | 227 | if (passCount === 0) { 228 | host.log(`Using TypeScript ${project.ts.version}`); 229 | } 230 | 231 | const [textChangesByFile, noAppliedFixes] = await getCodeFixesFromProject(project, opt, host, validFiles); 232 | 233 | allNoAppliedChangesByFile = getAllNoAppliedChangesByFile(allNoAppliedChangesByFile, noAppliedFixes); 234 | 235 | const changedFiles = await getChangedFiles(project, textChangesByFile); 236 | changedFiles.forEach((change, fileName) => { 237 | allChangedFiles.set(fileName, change); 238 | }); 239 | 240 | if (!textChangesByFile.size) { 241 | host.log(`No changes remaining for ts-fix`); 242 | break; 243 | } 244 | 245 | //Limit the number of passes 246 | if (passCount === 5) { 247 | break; 248 | } 249 | 250 | passCount++; 251 | } 252 | 253 | printSummary(host, opt, invalidFiles, allChangedFiles, allNoAppliedChangesByFile); 254 | 255 | return "Done"; 256 | } 257 | 258 | // TODO - Look into what other fixes fall into this category 259 | // Finds the fixes that don't make any changes (install module), and out of scope of the use case such as import, fixMissingFunctionDeclaration, fixMissingMember, spelling 260 | function isNotAppliedFix(fixAndDiagnostic: FixAndDiagnostic): boolean { 261 | return !fixAndDiagnostic.fix.changes.length 262 | || fixAndDiagnostic.fix.fixName === 'import' 263 | || fixAndDiagnostic.fix.fixName === 'fixMissingFunctionDeclaration' 264 | || fixAndDiagnostic.fix.fixName === 'fixMissingMember' 265 | || fixAndDiagnostic.fix.fixName === 'spelling'; 266 | } 267 | 268 | function getAppliedAndNoAppliedFixes(flatCodeFixesAndDiagnostics: FixAndDiagnostic[]): [FixAndDiagnostic[], CodeFixAction[]] { 269 | let fixesAndDiagnostics: FixAndDiagnostic[] = []; 270 | let noAppliedFixes: CodeFixAction[] = []; 271 | flatCodeFixesAndDiagnostics.forEach((fixAndDiagnostic) => { 272 | if (isNotAppliedFix(fixAndDiagnostic)) { 273 | noAppliedFixes.push(fixAndDiagnostic.fix); 274 | } 275 | else { 276 | fixesAndDiagnostics.push(fixAndDiagnostic); 277 | } 278 | }) 279 | return [fixesAndDiagnostics, noAppliedFixes] 280 | } 281 | 282 | function filterDuplicateCodefixes(fixesAndDiagnostics: FixAndDiagnostic[]): CodeFixAction[] { 283 | let stringifiedCodefixes: Set = new Set(); 284 | fixesAndDiagnostics.forEach((fixAndDiagnostic) => { 285 | addToCodefixes(stringifiedCodefixes, [fixAndDiagnostic.fix]); 286 | }) 287 | return Array.from(stringifiedCodefixes).map(codefix => JSON.parse(codefix)); 288 | } 289 | 290 | export async function getCodeFixesFromProject(project: Project, opt: Options, host: Host, validFiles: string[]): Promise<[Map, CodeFixAction[]]> { 291 | let codefixes: readonly CodeFixAction[] = []; 292 | 293 | // pull all codefixes. 294 | const diagnosticsPerFile = getDiagnostics(project); 295 | if (!diagnosticsPerFile.length) { 296 | host.log(`No more diagnostics.`); 297 | return [new Map(), []] 298 | } 299 | 300 | // Filter fixes and diagnostics according to the flags passed in 301 | let fixesAndDiagnostics: FixAndDiagnostic[] = []; 302 | let noAppliedFixes: CodeFixAction[] = []; 303 | 304 | // If errorCode is specified, only pull fixes for that/those error(s) 305 | // If file is specified, only pull fixes for that/those file(s) 306 | // Otherwise, pull all diagnostics 307 | let [filteredDiagnostics, acceptedDiagnosticsOut] = filterDiagnosticsByFileAndErrorCode(diagnosticsPerFile, opt.errorCode, validFiles); 308 | acceptedDiagnosticsOut.forEach((s: string) => { host.log(`${host.getNewLine()}${s}`); }); 309 | 310 | const codefixesPerFile = filteredDiagnostics.map(function (d: readonly Diagnostic[]) { 311 | const fixesAndDiagnostics = (getCodeFixesForFile(project, d)); 312 | return fixesAndDiagnostics; 313 | }); 314 | 315 | const flatCodeFixesAndDiagnostics: FixAndDiagnostic[] = _.flatten(codefixesPerFile); 316 | let [filteredCodeFixesAndDiagnostics, filteredCodeFixByNameOut] = filterCodeFixesByFixName(flatCodeFixesAndDiagnostics, opt.fixName); 317 | filteredCodeFixByNameOut.forEach((s: string) => { host.log(s); }); 318 | [fixesAndDiagnostics, noAppliedFixes] = getAppliedAndNoAppliedFixes(filteredCodeFixesAndDiagnostics); 319 | if (!opt.showMultiple) { 320 | fixesAndDiagnostics = removeMultipleDiagnostics(fixesAndDiagnostics); 321 | } 322 | fixesAndDiagnostics = removeDuplicatedFixes(fixesAndDiagnostics); 323 | if (filteredCodeFixesAndDiagnostics.length) { 324 | host.log(`Fixes to be applied: ${fixesAndDiagnostics.length}${host.getNewLine()}No applied fixes: ${noAppliedFixes.length}${host.getNewLine()}`); 325 | } 326 | 327 | //If interactive mode is enabled the user needs to decide which quick fixes to apply, this is handled here 328 | if (opt.interactiveMode) { 329 | let stringifiedCodefixes: Set = await getFileFixes(project, host, fixesAndDiagnostics, opt.showMultiple); 330 | codefixes = Array.from(stringifiedCodefixes).map(codefix => JSON.parse(codefix)); 331 | } 332 | else { 333 | // If interactiveMode = false, then remove duplicated codefixes and move forward 334 | codefixes = filterDuplicateCodefixes(fixesAndDiagnostics); 335 | } 336 | // Organize textChanges by what file they alter 337 | const textChangesByFile = getTextChangeDict(codefixes); 338 | return [textChangesByFile, noAppliedFixes]; 339 | } 340 | 341 | export function getDiagnostics(project: Project): (readonly Diagnostic[])[] { 342 | const compilerOptions = project.program.getCompilerOptions(); 343 | const diagnostics = project.program.getSourceFiles().map(function (file: SourceFile) { 344 | return [ 345 | ...(compilerOptions.declaration || compilerOptions.composite ? project.program.getDeclarationDiagnostics(file) : []), 346 | ...project.program.getSemanticDiagnostics(file), 347 | ]; 348 | }); 349 | return diagnostics; 350 | } 351 | 352 | export function filterDiagnosticsByFileAndErrorCode(diagnostics: (readonly Diagnostic[])[], errorCodes: number[], validFiles?: string[]): [(readonly Diagnostic[])[], string[]] { 353 | // if files were passed in, only use the specified files 354 | // if errorCodes were passed in, only use the specified errors 355 | // diagnostics is guaranteed to not be [] or [[]] 356 | let filteredDiagnostics = diagnostics.filter((diagnostics) => diagnostics.length); 357 | let returnStrings: string[] = []; 358 | 359 | // Check if file is in node_modules, if so remove it from filteredDiagnostics, we'll not be applying fixes there 360 | filteredDiagnostics = filteredDiagnostics.filter((filteredDiagnostic) => { 361 | return !(filteredDiagnostic[0].file?.fileName && /[\\/]node_modules[\\/]/.test(filteredDiagnostic[0].file?.fileName)); 362 | }) 363 | 364 | if (errorCodes.length || validFiles?.length) { 365 | if (validFiles?.length) { 366 | let filteredDiagnosticsForFile: Diagnostic[][] = []; 367 | let length = 0; 368 | let j = -1; 369 | for (let i = 0; i < filteredDiagnostics.length; i++) { 370 | if (filteredDiagnostics[i][0].file?.fileName && validFiles.includes(path.normalize(filteredDiagnostics[i][0].file?.fileName!))) { 371 | let index = 0; 372 | j++; 373 | filteredDiagnosticsForFile[j] = new Array; 374 | filteredDiagnostics[i].filter((diagnostic) => { 375 | filteredDiagnosticsForFile[j][index] = diagnostic; 376 | length++; 377 | index++; 378 | }); 379 | } 380 | } 381 | if (length === 0) { 382 | returnStrings.push(`No diagnostics found for files`); 383 | } 384 | else { 385 | returnStrings.push(`Found ${length} diagnostics for the given files`); 386 | } 387 | filteredDiagnostics = filteredDiagnosticsForFile; 388 | } 389 | if (errorCodes.length) { 390 | let errorCounter = new Map(); 391 | let filteredDiagnosticsByError: Diagnostic[][] = []; 392 | for (let i = 0; i < diagnostics.length; i++) { 393 | // for every diagnostic list 394 | // get rid of not matched errors 395 | const filteredDiagnostic = _.filter(filteredDiagnostics[i], function (d) { 396 | if (errorCodes.includes(d.code)) { 397 | const currentVal = errorCounter.get(d.code); 398 | if (currentVal !== undefined) { 399 | errorCounter.set(d.code, currentVal + 1); 400 | } else { 401 | errorCounter.set(d.code, 1); 402 | } 403 | return true; 404 | } 405 | return false; 406 | }); 407 | if (filteredDiagnostic.length) { 408 | filteredDiagnosticsByError.push(filteredDiagnostic); 409 | } 410 | } 411 | errorCodes.forEach((code: number) => { 412 | const count = errorCounter.get(code); 413 | if (count === undefined) { 414 | returnStrings.push(`No diagnostics found with code ${code}`); 415 | } else { 416 | returnStrings.push(`Found ${count} diagnostics with code ${code}`); 417 | } 418 | }); 419 | filteredDiagnostics = filteredDiagnosticsByError; 420 | } 421 | return [filteredDiagnostics, returnStrings] 422 | } 423 | // otherwise, use all errors 424 | return [filteredDiagnostics, [`Found ${_.reduce(filteredDiagnostics.map((d: { length: any; }) => d.length), function (sum, n) { 425 | return sum + n; 426 | }, 0)} diagnostics in ${diagnostics.length} files`]]; 427 | } 428 | 429 | export interface FixAndDiagnostic { 430 | fix: CodeFixAction; 431 | diagnostic: Diagnostic; 432 | } 433 | 434 | export function getCodeFixesForFile(project: Project, diagnostics: readonly Diagnostic[]): FixAndDiagnostic[] { 435 | // expects already filtered diagnostics 436 | const service = project.languageService; 437 | return flatMap(diagnostics, d => { 438 | if (d.file && typeof d.start === "number" && d.length) { 439 | return service.getCodeFixesAtPosition( 440 | d.file.fileName, 441 | d.start, 442 | d.start + d.length, 443 | [d.code], 444 | project.ts.getDefaultFormatCodeSettings(os.EOL), 445 | {}).map((fix: CodeFixAction) => { 446 | return { fix, diagnostic: d }; 447 | }); 448 | } else { 449 | return []; 450 | } 451 | }) 452 | } 453 | 454 | function getFileTextChangesFromCodeFix(codefix: CodeFixAction): readonly FileTextChanges[] { 455 | return codefix.changes; 456 | } 457 | 458 | function mergeChanges(arr1: TextChange[], arr2: TextChange[]): TextChange[] { 459 | let mergedArray = []; 460 | let i = 0; 461 | let j = 0; 462 | 463 | while (i < arr1.length && j < arr2.length) { 464 | if (arr1[i].span.start < arr2[j].span.start) { 465 | mergedArray.push(arr1[i]); 466 | i++; 467 | } else { 468 | mergedArray.push(arr2[j]); 469 | j++; 470 | } 471 | } 472 | 473 | while (i < arr1.length) { 474 | mergedArray.push(arr1[i]); 475 | i++; 476 | } 477 | 478 | while (j < arr2.length) { 479 | mergedArray.push(arr2[j]); 480 | j++; 481 | } 482 | 483 | return mergedArray; 484 | } 485 | export function getTextChangeDict(codefixes: readonly CodeFixAction[]): Map { 486 | let textChangeDict = new Map(); 487 | 488 | for (let i = 0; i < codefixes.length; i++) { 489 | const fix = codefixes[i]; 490 | const changes = getFileTextChangesFromCodeFix(fix); 491 | 492 | for (let j = 0; j < changes.length; j++) { 493 | let change = changes[j]; 494 | let validChanges = getFileNameAndTextChangesFromCodeFix(change); 495 | if (validChanges === undefined) { 496 | continue 497 | } 498 | let [key, value] = validChanges; 499 | const prevVal = textChangeDict.get(key); 500 | if (prevVal === undefined) { 501 | textChangeDict.set(key, value); 502 | } else { 503 | textChangeDict.set(key, mergeChanges(prevVal, value)); 504 | } 505 | } 506 | } 507 | 508 | return textChangeDict; 509 | } 510 | 511 | export function filterCodeFixesByFixName(codeFixesAndDiagnostics: FixAndDiagnostic[], fixNames: string[]): [FixAndDiagnostic[], string[]] { 512 | if (fixNames.length === 0) { 513 | // empty argument behavior... currently, we just keep all fixes if none are specified 514 | return [codeFixesAndDiagnostics, [`Found ${codeFixesAndDiagnostics.length} codefixes`]]; 515 | } 516 | // cannot sort by fixId right now since fixId is a {} 517 | // do we want to distinguish the case when no codefixes are picked? (no hits) 518 | 519 | let fixCounter = new Map(); 520 | let out: string[] = []; 521 | const filteredFixesAndDiagnostics = codeFixesAndDiagnostics.filter((codeFixAndDiagnostic) => { 522 | if (codeFixAndDiagnostic.fix && fixNames.includes(codeFixAndDiagnostic.fix.fixName)) { 523 | const currentVal = fixCounter.get(codeFixAndDiagnostic.fix.fixName); 524 | if (currentVal !== undefined) { 525 | fixCounter.set(codeFixAndDiagnostic.fix.fixName, currentVal + 1); 526 | } else { 527 | fixCounter.set(codeFixAndDiagnostic.fix.fixName, 1); 528 | } 529 | return true; 530 | } 531 | return false; 532 | }); 533 | 534 | fixNames.forEach((name: string) => { 535 | const count = fixCounter.get(name); 536 | if (count === undefined) { 537 | out.push(`No codefixes found with name ${name}`) 538 | } else { 539 | out.push(`Found ${count} codefixes with name ${name}`); 540 | } 541 | }); 542 | 543 | return [filteredFixesAndDiagnostics, out]; 544 | } 545 | 546 | function getFileNameAndTextChangesFromCodeFix(ftchanges: FileTextChanges): [string, TextChange[]] | undefined { 547 | if (/[\\/]node_modules[\\/]/.test(ftchanges.fileName)) { 548 | return undefined; 549 | } 550 | return [ftchanges.fileName, [...ftchanges.textChanges]]; 551 | } 552 | 553 | export interface ChangedFile { 554 | originalText: string; 555 | newText: string; 556 | } 557 | 558 | async function getUpdatedCodeFixesAndDiagnostics(codeFixesAndDiagnostics: FixAndDiagnostic[], fixesInTheSameSpan: FixAndDiagnostic[], codefixes: Set, count: number, showMultiple?: boolean): Promise<[FixAndDiagnostic[], Set]> { 559 | const currentDiagnostic = codeFixesAndDiagnostics[0].diagnostic; 560 | const currentCodeFix = codeFixesAndDiagnostics[0].fix; 561 | const userInput = fixesInTheSameSpan.length ? await getUserPickFromMultiple({ currentFixesAndDiagnostics: fixesInTheSameSpan, isSameSpan: true }) 562 | : await getUserPickFromMultiple({ codefix: codeFixesAndDiagnostics[0].fix, showMultiple }); 563 | if (userInput === Choices.ACCEPT) { 564 | addToCodefixes(codefixes, [currentCodeFix]); 565 | if (showMultiple) { 566 | codeFixesAndDiagnostics = removeMultipleDiagnostics(codeFixesAndDiagnostics, Choices.ACCEPT) 567 | } 568 | else { 569 | codeFixesAndDiagnostics.splice(0, count); 570 | }; 571 | } 572 | else if (userInput === Choices.ACCEPTALL) { 573 | if (showMultiple) { 574 | codeFixesAndDiagnostics = removeMultipleDiagnostics(codeFixesAndDiagnostics); 575 | } 576 | let updatedFixesAndDiagnostics = codeFixesAndDiagnostics.filter((diagnosticAndFix) => diagnosticAndFix.diagnostic.code === currentDiagnostic.code); 577 | updatedFixesAndDiagnostics.map((diagnosticAndFix) => { 578 | return addToCodefixes(codefixes, [diagnosticAndFix.fix]); 579 | }); 580 | codeFixesAndDiagnostics = codeFixesAndDiagnostics.filter((diagnosticAndFix) => diagnosticAndFix.diagnostic.code !== codeFixesAndDiagnostics[0].diagnostic.code); 581 | } 582 | else if (userInput === Choices.SKIPALL) { 583 | let updatedFixesAndDiagnostics = codeFixesAndDiagnostics.filter((diagnosticAndFix) => diagnosticAndFix.diagnostic.code === currentDiagnostic.code); 584 | updatedFixesAndDiagnostics.forEach(diagnosticAndFix => codeFixesAndDiagnostics.splice(codeFixesAndDiagnostics.findIndex(fixAndDiagnostic => fixAndDiagnostic.diagnostic.code === diagnosticAndFix.diagnostic.code), count)) 585 | } 586 | else if (userInput === Choices.SKIP) { 587 | codeFixesAndDiagnostics.splice(0, count); 588 | } 589 | else if (userInput === Choices.SHOWMULTIPLE) { 590 | const chooseCodeFix = await getUserPickFromMultiple({ currentFixesAndDiagnostics: codeFixesAndDiagnostics.slice(0, count) }) 591 | if (codeFixesAndDiagnostics.filter((codeFixAndDiagnostic) => `"${codeFixAndDiagnostic.fix.fixName}" fix with description "${codeFixAndDiagnostic.fix.description}"` === chooseCodeFix).length) { 592 | addToCodefixes(codefixes, [codeFixesAndDiagnostics.filter((codeFixAndDiagnostic) => `"${codeFixAndDiagnostic.fix.fixName}" fix with description "${codeFixAndDiagnostic.fix.description}"` === chooseCodeFix)[0].fix]); 593 | codeFixesAndDiagnostics.splice(0, count); 594 | } 595 | } 596 | else { 597 | if (codeFixesAndDiagnostics.filter((codeFixAndDiagnostic) => { 598 | let newText = map(codeFixAndDiagnostic.fix.changes[0].textChanges, 'newText').join(" ").trim(); 599 | return `"${codeFixAndDiagnostic.fix.fixName}" fix with new text "${newText}"`;; 600 | })) { 601 | addToCodefixes(codefixes, [codeFixesAndDiagnostics.filter((codeFixAndDiagnostic) => { 602 | let newText = map(codeFixAndDiagnostic.fix.changes[0].textChanges, 'newText').join(" ").trim(); 603 | return `"${codeFixAndDiagnostic.fix.fixName}" fix with new text "${newText}"`;; 604 | })[0].fix]); 605 | codeFixesAndDiagnostics.splice(0, count); 606 | } 607 | } 608 | return [codeFixesAndDiagnostics, codefixes]; 609 | } 610 | 611 | // Adds new codefixes to the codefixes array but first check if that fix already exists on it 612 | function addToCodefixes(codefixes: Set, newCodefixes: CodeFixAction[]): Set { 613 | newCodefixes.forEach((newCodefix) => { 614 | if (!codefixes.has(JSON.stringify(newCodefix))) { 615 | codefixes.add(JSON.stringify(newCodefix)); 616 | } 617 | }) 618 | return codefixes; 619 | } 620 | 621 | async function getUserPickFromMultiple(args: { codefix?: CodeFixAction, currentFixesAndDiagnostics?: FixAndDiagnostic[], isSameSpan?: boolean, showMultiple?: boolean }): Promise { 622 | let choices: string[] = []; 623 | let message: string = ""; 624 | if (args.codefix && args.showMultiple) { 625 | message = `Would you like to apply the ${args.codefix.fixName} fix with description "${args.codefix.description}"?`; 626 | choices = [Choices.ACCEPT, Choices.ACCEPTALL, Choices.SKIPALL, Choices.SKIP, Choices.SHOWMULTIPLE]; 627 | } 628 | else if (args.codefix && !args.showMultiple) { 629 | message = `Would you like to apply the ${args.codefix.fixName} fix with description "${args.codefix.description}"?`; 630 | choices = [Choices.ACCEPT, Choices.ACCEPTALL, Choices.SKIPALL, Choices.SKIP]; 631 | } 632 | else if (args.isSameSpan && args.currentFixesAndDiagnostics) { 633 | message = `Which fix would you like to apply?`; 634 | choices = args.currentFixesAndDiagnostics.map((codeFixAndDiagnostic) => { 635 | let newText = map(codeFixAndDiagnostic.fix.changes[0].textChanges, 'newText').join(" ").trim(); 636 | return `"${codeFixAndDiagnostic.fix.fixName}" fix with new text "${newText}"`; 637 | }); 638 | } 639 | else if (args.currentFixesAndDiagnostics) { 640 | message = `Which fix would you like to apply?`; 641 | choices = args.currentFixesAndDiagnostics.map((codeFixAndDiagnostic) => { 642 | return `"${codeFixAndDiagnostic.fix.fixName}" fix with description "${codeFixAndDiagnostic.fix.description}"` 643 | }); 644 | } 645 | 646 | const choice = await inquirer 647 | .prompt([ 648 | { 649 | name: "userInput", 650 | type: "list", 651 | message: message, 652 | choices: choices 653 | }, 654 | ]); 655 | return choice.userInput; 656 | }; 657 | 658 | export interface ChangeDiagnostic { 659 | file?: SourceFile, 660 | start?: number, 661 | length?: number, 662 | } 663 | 664 | // Removes duplicate diagnostics if showMultiple = false 665 | // Also removes multiple code fixes for the diagnostic if the user accepts the first one or to accept all of that type 666 | export function removeMultipleDiagnostics(codeFixesAndDiagnostics: FixAndDiagnostic[], choice?: Choices.ACCEPT): FixAndDiagnostic[] { 667 | if (choice === Choices.ACCEPT) { 668 | for (let i = 1; i < codeFixesAndDiagnostics.length; i++) { 669 | let count = 1; 670 | if (codeFixesAndDiagnostics[0].diagnostic.code === codeFixesAndDiagnostics[i].diagnostic.code && codeFixesAndDiagnostics[0].diagnostic.length === codeFixesAndDiagnostics[i].diagnostic.length && codeFixesAndDiagnostics[0].diagnostic.start === codeFixesAndDiagnostics[i].diagnostic.start) { 671 | let j = i; 672 | while (codeFixesAndDiagnostics[j] && codeFixesAndDiagnostics[0].diagnostic.code === codeFixesAndDiagnostics[j].diagnostic.code && codeFixesAndDiagnostics[0].diagnostic.length === codeFixesAndDiagnostics[j].diagnostic.length && codeFixesAndDiagnostics[0].diagnostic.start === codeFixesAndDiagnostics[j].diagnostic.start) { 673 | j++; 674 | count++; 675 | } 676 | codeFixesAndDiagnostics.splice(0, count); 677 | } 678 | } 679 | } 680 | else { 681 | for (let i = 0; i < codeFixesAndDiagnostics.length; i++) { 682 | let count = 1; 683 | if (codeFixesAndDiagnostics[i + 1] && codeFixesAndDiagnostics[i].diagnostic.code === codeFixesAndDiagnostics[i + 1].diagnostic.code && codeFixesAndDiagnostics[i].diagnostic.length === codeFixesAndDiagnostics[i + 1].diagnostic.length && codeFixesAndDiagnostics[i].diagnostic.start === codeFixesAndDiagnostics[i + 1].diagnostic.start) { 684 | let j = i; 685 | while (codeFixesAndDiagnostics[j + 1] && codeFixesAndDiagnostics[j].diagnostic.code === codeFixesAndDiagnostics[j + 1].diagnostic.code && codeFixesAndDiagnostics[j].diagnostic.length === codeFixesAndDiagnostics[j + 1].diagnostic.length && codeFixesAndDiagnostics[j].diagnostic.start === codeFixesAndDiagnostics[j + 1].diagnostic.start) { 686 | j++; 687 | count++; 688 | } 689 | codeFixesAndDiagnostics.splice(i + 1, count - 1); 690 | } 691 | } 692 | } 693 | return codeFixesAndDiagnostics; 694 | } 695 | 696 | // Removes duplicate code fixes 697 | export function removeDuplicatedFixes(codeFixesAndDiagnostics: FixAndDiagnostic[]): FixAndDiagnostic[] { 698 | for (let i = 0; i < codeFixesAndDiagnostics.length; i++) { 699 | for (let j = i + 1; j < codeFixesAndDiagnostics.length; j++) { 700 | if (i !== j && isEqual(codeFixesAndDiagnostics[i].fix, codeFixesAndDiagnostics[j].fix) && isEqual(codeFixesAndDiagnostics[i].diagnostic.messageText, codeFixesAndDiagnostics[j].diagnostic.messageText)) { 701 | codeFixesAndDiagnostics.splice(j, 1); 702 | j = j - 1; 703 | } 704 | } 705 | } 706 | return codeFixesAndDiagnostics; 707 | } 708 | 709 | function getSecondDiagnostic(project: Project, fileName: string, currentCodeFix: CodeFixAction, currentTextChanges: readonly TextChange[]): ChangeDiagnostic { 710 | let secondDiagnostic: ChangeDiagnostic = {}; 711 | let secondFileContents = undefined; 712 | let secondFileCurrentLineMap = undefined; 713 | const newSourceFile = project.program.getSourceFile(fileName); 714 | const secondSourceFile = cloneDeep(newSourceFile) 715 | if (secondSourceFile) { 716 | secondDiagnostic.file = secondSourceFile; 717 | secondDiagnostic.start = currentCodeFix.changes[0].textChanges[0].span.start; 718 | secondDiagnostic.length = currentCodeFix.changes[0].textChanges[0].newText.length; 719 | } 720 | secondFileContents = secondSourceFile ? secondSourceFile.text : undefined; 721 | secondFileCurrentLineMap = secondSourceFile ? (secondSourceFile as any).lineMap : undefined; 722 | (secondSourceFile as any).lineMap = undefined; 723 | if (secondFileContents) { 724 | const newFileContents = applyChangestoFile(secondFileContents, currentTextChanges, true); 725 | if (secondDiagnostic.file) { 726 | secondDiagnostic.file.text = newFileContents; 727 | } 728 | } 729 | return secondDiagnostic; 730 | } 731 | 732 | // There are more than one quick fix for the same diagnostic 733 | // If the --showMultiple flag is being used then ask the user which fix to apply, otherwise apply the first fix given for the diagnostic every time 734 | function isAdditionalFixForSameDiagnostic(codeFixesAndDiagnostics: FixAndDiagnostic[], first: number): boolean { 735 | if (codeFixesAndDiagnostics[first + 1]) { 736 | const firstDiagnostic = codeFixesAndDiagnostics[first].diagnostic; 737 | const secondDiagnostic = codeFixesAndDiagnostics[first + 1].diagnostic; 738 | return firstDiagnostic.code === secondDiagnostic.code && firstDiagnostic.length === secondDiagnostic.length && firstDiagnostic.start === secondDiagnostic.start; 739 | } 740 | return false; 741 | } 742 | 743 | // There is more than one fix being applied on the same line (eg. infer parameter types from usage) 744 | // If there is more than one fix being applied on the same line we should display them to the user at the same time 745 | function isAdditionalFixInTheSameLine(codeFixesAndDiagnostics: FixAndDiagnostic[], first: number): boolean { 746 | if (codeFixesAndDiagnostics[first + 1]) { 747 | const firstDiagnosticAndFix = codeFixesAndDiagnostics[first]; 748 | const secondDiagnosticAndFix = codeFixesAndDiagnostics[first + 1]; 749 | return firstDiagnosticAndFix.diagnostic.messageText !== secondDiagnosticAndFix.diagnostic.messageText 750 | && firstDiagnosticAndFix.fix.changes[0].fileName === secondDiagnosticAndFix.fix.changes[0].fileName 751 | && firstDiagnosticAndFix.fix.changes[0].textChanges.length === secondDiagnosticAndFix.fix.changes[0].textChanges.length 752 | && isEqual(firstDiagnosticAndFix.fix.changes[0].textChanges, secondDiagnosticAndFix.fix.changes[0].textChanges); 753 | } 754 | return false; 755 | } 756 | 757 | function isFixAppliedToTheSameSpan(codeFixesAndDiagnostics: FixAndDiagnostic[], first: number, second: number) { 758 | if (codeFixesAndDiagnostics[second]) { 759 | const firstDiagnosticAndFix = codeFixesAndDiagnostics[first]; 760 | const secondDiagnosticAndFix = codeFixesAndDiagnostics[second]; 761 | return firstDiagnosticAndFix.diagnostic.code === secondDiagnosticAndFix.diagnostic.code 762 | && firstDiagnosticAndFix.fix.changes[0].fileName === secondDiagnosticAndFix.fix.changes[0].fileName 763 | && firstDiagnosticAndFix.fix.changes[0].textChanges[0].newText !== secondDiagnosticAndFix.fix.changes[0].textChanges[0].newText 764 | && firstDiagnosticAndFix.fix.changes[0].textChanges[0].span.start === secondDiagnosticAndFix.fix.changes[0].textChanges[0].span.start; 765 | } 766 | return false; 767 | } 768 | 769 | export async function displayDiagnostic(project: Project, host: Host, codeFixesAndDiagnostics: FixAndDiagnostic[], codefixes: Set, showMultiple: boolean): Promise<[FixAndDiagnostic[], Set]> { 770 | let currentUpdatedCodeFixes: Set = new Set(); 771 | let updatedFixesAndDiagnostics: FixAndDiagnostic[] = []; 772 | const currentCodeFix: CodeFixAction = codeFixesAndDiagnostics[0].fix; 773 | const currentDiagnostic = codeFixesAndDiagnostics[0].diagnostic; 774 | let diagnosticsInTheSameLine: Diagnostic[] = []; 775 | let fixesInTheSameSpan: FixAndDiagnostic[] = []; 776 | let count = 1; 777 | 778 | // Check if there are additional fixes for the same diagnostic 779 | if (isAdditionalFixForSameDiagnostic(codeFixesAndDiagnostics, 0)) { 780 | let j = 0; 781 | while (isAdditionalFixForSameDiagnostic(codeFixesAndDiagnostics, j)) { 782 | j++; 783 | count++; 784 | } 785 | } 786 | // Then check if there's more than one fix being applied on the same line 787 | else if (isAdditionalFixInTheSameLine(codeFixesAndDiagnostics, 0)) { 788 | let j = 0; 789 | diagnosticsInTheSameLine.push(codeFixesAndDiagnostics[j].diagnostic); 790 | while (isAdditionalFixInTheSameLine(codeFixesAndDiagnostics, j)) { 791 | j++; 792 | count++; 793 | diagnosticsInTheSameLine.push(codeFixesAndDiagnostics[j].diagnostic); 794 | } 795 | } 796 | // Then check if there's more than one consecutive fix being applied to the same span 797 | // TODO: This should support more than just consecutive fixes that affect the same span 798 | else if (isFixAppliedToTheSameSpan(codeFixesAndDiagnostics, 0, 1)) { 799 | let j = 0; 800 | fixesInTheSameSpan.push(codeFixesAndDiagnostics[j]); 801 | while (isFixAppliedToTheSameSpan(codeFixesAndDiagnostics, j, j + 1)) { 802 | j++; 803 | count++; 804 | fixesInTheSameSpan.push(codeFixesAndDiagnostics[j]); 805 | } 806 | } 807 | 808 | if (codeFixesAndDiagnostics[0].fix.changes.length > 1) { 809 | // TODO Implement, Haven't seen a case where this is true yet 810 | host.log("The fix.changes array contains more than one element"); 811 | } 812 | else { 813 | let sourceFile = currentDiagnostic.file?.fileName ? project.program.getSourceFile(currentDiagnostic.file?.fileName) : undefined; 814 | let currentTextChanges: readonly TextChange[] = currentCodeFix.changes[0].textChanges; 815 | const changesFileName = currentCodeFix.changes[0].fileName; 816 | let changesOnDifferentLocation = false; 817 | if (sourceFile !== undefined) { 818 | let secondDiagnostic: ChangeDiagnostic = {}; // Needed when changes are made on a different file or in the same file but a different location 819 | const originalContents = sourceFile.text; 820 | const currentLineMap = (sourceFile as any).lineMap; 821 | // Changes are made on a different file 822 | if (currentDiagnostic.file?.fileName !== changesFileName) { 823 | changesOnDifferentLocation = true; 824 | secondDiagnostic = getSecondDiagnostic(project, changesFileName, currentCodeFix, currentTextChanges); 825 | } 826 | else { 827 | let matchesDiagnosticStartAndLength = false; 828 | if (currentTextChanges.length === 1) { 829 | matchesDiagnosticStartAndLength = currentDiagnostic.start && currentDiagnostic.length ? currentTextChanges[0].span.start === currentDiagnostic.start + currentDiagnostic.length || currentTextChanges[0].span.start === currentDiagnostic.start : false; 830 | } 831 | // If there are more than one fix 832 | if (currentTextChanges.length > 1) { 833 | // First check if there's a text change with the start that matches the diagnostic start + length 834 | // Second check if there's a text change that matches the start but not start + length 835 | matchesDiagnosticStartAndLength = currentTextChanges.find((textChange) => { 836 | return (currentDiagnostic.start && currentDiagnostic.length && textChange.span.start === currentDiagnostic.start + currentDiagnostic.length); 837 | }) ? true : currentTextChanges.find((textChange) => { 838 | return (currentDiagnostic.start && currentDiagnostic.length && textChange.span.start === currentDiagnostic.start); 839 | }) ? true : false; 840 | } 841 | // Changes are made in the same file but on a different location 842 | if (!matchesDiagnosticStartAndLength) { 843 | changesOnDifferentLocation = true; 844 | secondDiagnostic = getSecondDiagnostic(project, changesFileName, currentCodeFix, currentTextChanges); 845 | } 846 | } 847 | // Changes are made in the same file in the same location where the diagnostic is 848 | if (!changesOnDifferentLocation && !fixesInTheSameSpan.length) { 849 | (sourceFile as any).lineMap = undefined; 850 | const newFileContents = applyChangestoFile(originalContents, currentTextChanges, true); 851 | if (currentDiagnostic.file) { 852 | currentDiagnostic.file.text = newFileContents; 853 | } 854 | } 855 | 856 | // Fixes affect the same text span 857 | if (fixesInTheSameSpan.length) { 858 | host.log(`There's more than one code fix affecting the same span, please review below:`); 859 | host.log(formatFixesInTheSameSpan(fixesInTheSameSpan, host)); 860 | } 861 | // More than one fix in the same line 862 | else if (diagnosticsInTheSameLine.length) { 863 | host.log(`There's more than one code fix in the same line, please review below:`); 864 | host.log(formatDiagnosticsWithColorAndContextTsFix(diagnosticsInTheSameLine, host)) 865 | } 866 | // Single diagnostic 867 | else { 868 | host.log(formatDiagnosticsWithColorAndContext([currentDiagnostic], host)); 869 | } 870 | // Display fix on different location 871 | if (changesOnDifferentLocation && secondDiagnostic && !fixesInTheSameSpan.length) { 872 | host.log(`Please review the fix below for the diagnostic above`); 873 | host.log(formatFixOnADifferentLocation([secondDiagnostic], host)); 874 | } 875 | // Get user's choice and updated fixes and diagnostics 876 | const isShowMultiple = showMultiple && count > 1 && !diagnosticsInTheSameLine.length; 877 | [updatedFixesAndDiagnostics, currentUpdatedCodeFixes] = await getUpdatedCodeFixesAndDiagnostics(codeFixesAndDiagnostics, fixesInTheSameSpan, codefixes, count, isShowMultiple); 878 | // Reset text and lineMap to their original contents 879 | if (currentDiagnostic.file) { 880 | currentDiagnostic.file.text = originalContents; 881 | } 882 | (sourceFile as any).lineMap = currentLineMap; 883 | } 884 | } 885 | return [updatedFixesAndDiagnostics, currentUpdatedCodeFixes]; 886 | } 887 | 888 | export async function getFileFixes(project: Project, host: Host, codeFixesAndDiagnostics: FixAndDiagnostic[], showMultiple: boolean): Promise> { 889 | let codefixes: Set = new Set(); 890 | let updatedFixesAndDiagnostics: FixAndDiagnostic[] = []; 891 | let currentUpdatedCodeFixes: Set = new Set() 892 | for (let i = 0; i < codeFixesAndDiagnostics.length; i++) { 893 | [updatedFixesAndDiagnostics, currentUpdatedCodeFixes] = await displayDiagnostic(project, host, codeFixesAndDiagnostics, codefixes, showMultiple); 894 | codeFixesAndDiagnostics = updatedFixesAndDiagnostics; 895 | codefixes = currentUpdatedCodeFixes; 896 | i = -1; 897 | } 898 | return codefixes; 899 | } 900 | 901 | async function getChangedFiles(project: Project, textChanges: Map): Promise> { 902 | const changedFiles = new Map(); 903 | 904 | for (let [fileName, textChange] of textChanges.entries()) { 905 | const sourceFile = project.program.getSourceFile(fileName); 906 | let fileFix = textChange; 907 | if (sourceFile !== undefined) { 908 | const originalFileContents = sourceFile.text; 909 | 910 | const newFileContents = applyChangestoFile(originalFileContents, fileFix); 911 | changedFiles.set(fileName, { originalText: originalFileContents, newText: newFileContents }); 912 | } 913 | else { 914 | throw new Error(`File ${fileName} not found in project`); 915 | } 916 | }; 917 | 918 | return changedFiles; 919 | } 920 | 921 | export enum Choices { 922 | ACCEPT = 'Accept', 923 | ACCEPTALL = 'Accept all quick fixes for this diagnostic code in this pass', 924 | SKIPALL = 'Skip this diagnostic code this pass', 925 | SKIP = 'Skip', 926 | SHOWMULTIPLE = 'Show more quick fixes for this diagnostic' 927 | } 928 | 929 | function applyChangestoFile(originalContents: string, fixList: readonly TextChange[], isGetFileFixes?: boolean): string { 930 | // maybe we want to have this and subsequent functions to return a diagnostic 931 | // function expects fixList to be already sorted and filtered 932 | const newFileContents = doTextChanges(originalContents, fixList, isGetFileFixes); 933 | return newFileContents; 934 | } 935 | 936 | export function doTextChanges(fileText: string, textChanges: readonly TextChange[], isGetFileFixes?: boolean): string { 937 | const tcs: TextChange[] = []; 938 | // Remove duplicate text changes. Because we're getting quick fixes for the entire file, we can get duplicate text changes. 939 | for (const tc of textChanges) { 940 | const dup = tcs.find((t) => t.span.start === tc.span.start && t.span.length === tc.span.length && t.newText === tc.newText); 941 | if (!dup) { 942 | tcs.push(tc); 943 | } 944 | } 945 | 946 | // does js/ts do references? Or is it always a copy when you pass into a function 947 | // iterate through codefixes from back 948 | for (let i = tcs.length - 1; i >= 0; i--) { 949 | // apply each codefix 950 | fileText = doTextChangeOnString(fileText, tcs[i], isGetFileFixes); 951 | } 952 | return fileText; 953 | } 954 | 955 | export function doTextChangeOnString(currentFileText: string, change: TextChange, isGetFileFixes?: boolean): string { 956 | const prefix = currentFileText.substring(0, change.span.start); 957 | let middle = ""; 958 | if (isGetFileFixes) middle = compareContentsAndLog(currentFileText.substring(change.span.start, change.span.start + change.span.length), change.newText); 959 | else middle = change.newText; 960 | const suffix = currentFileText.substring(change.span.start + change.span.length); 961 | return prefix + middle + suffix; 962 | } 963 | 964 | function compareContentsAndLog(str1: string, str2: string): string { 965 | let diff = diffChars(str1, str2); 966 | let middleString = ""; 967 | let newLine = "\r\n"; 968 | diff.forEach((part) => { 969 | // green for additions, red for deletions 970 | // unset for common parts 971 | let color; 972 | if (part.added) color = 32; 973 | if (part.removed) color = 31; 974 | if (part.added || part.removed) { 975 | //Purely aesthetics 976 | if (part.value.startsWith(newLine) && part.value.endsWith(newLine)) { 977 | middleString += newLine; 978 | middleString += `\x1b[${color}m${part.value.substring(2, part.value.length - 2)}\x1b[0m`; 979 | middleString += newLine; 980 | } 981 | else if (part.value.endsWith(newLine)) { 982 | middleString += `\x1b[${color}m${part.value.substring(0, part.value.length - 2)}\x1b[0m`; 983 | middleString += newLine; 984 | } 985 | else if (part.value.startsWith(newLine)) { 986 | middleString += newLine; 987 | middleString += `\x1b[${color}m${part.value.substring(2)}\x1b[0m`; 988 | } 989 | else { 990 | middleString += `\x1b[${color}m${part.value}\x1b[0m`; 991 | } 992 | } 993 | else { 994 | middleString += `${part.value}`; 995 | } 996 | }); 997 | 998 | return middleString; 999 | } 1000 | 1001 | function hasOnlyEmptyLists(m: ReadonlyMap): boolean { 1002 | let arrayLength = 0; 1003 | for (const [_, entries] of m.entries()) { 1004 | if (entries.length) { 1005 | arrayLength++; 1006 | } 1007 | } 1008 | return arrayLength > 0 ? false : true; 1009 | } 1010 | 1011 | export function getFileName(filePath: string): string { 1012 | return path.basename(filePath); 1013 | } 1014 | 1015 | export function getDirectory(filePath: string): string { 1016 | return path.dirname(filePath); 1017 | } 1018 | 1019 | export function getRelativePath(filePath: string, opt: Options): string { 1020 | // this doesn't work when tsconfig or filepath is not passed in as absolute... 1021 | // as a result getOutputFilePath does not work for the non-replace option 1022 | return path.relative(getDirectory(opt.tsconfig), path.resolve(filePath)); 1023 | } 1024 | 1025 | export function getOutputFilePath(filePath: string, opt: Options): string { 1026 | // this function uses absolute paths 1027 | const fileName = getRelativePath(filePath, opt); 1028 | return path.resolve(opt.outputFolder, fileName); 1029 | } 1030 | 1031 | function writeToFile(fileName: string, fileContents: string, opt: Options, host: Host): string { 1032 | const writeToFileName = getOutputFilePath(fileName, opt); 1033 | const writeToDirectory = getDirectory(writeToFileName) 1034 | if (!host.exists(writeToDirectory)) { 1035 | host.mkdir(writeToDirectory); 1036 | } 1037 | host.writeFile(writeToFileName, fileContents); 1038 | host.log("Updated " + path.relative(opt.cwd, writeToFileName)); 1039 | return writeToFileName; 1040 | } 1041 | -------------------------------------------------------------------------------- /src/ts.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import importCwd from "import-cwd"; 3 | import type { LanguageService, LanguageServiceHost, ParseConfigFileHost, Program } from "typescript"; 4 | import { ChangedFile } from "."; 5 | 6 | function isTypeScriptVersionSupported(major: number, minor: number) { 7 | if (major < 3) return false; 8 | if (major < 4) return minor >= 5; 9 | return true; 10 | } 11 | 12 | type TypeScript = typeof import("typescript"); 13 | 14 | export function loadTypeScript(): TypeScript { 15 | try { 16 | const ts = importCwd("typescript") as typeof import("typescript"); 17 | const [major, minor] = ts.versionMajorMinor.split('.'); 18 | if (isTypeScriptVersionSupported(parseInt(major, 10), parseInt(minor, 10))) { 19 | return ts; 20 | } 21 | } catch {} 22 | return require("typescript"); 23 | } 24 | 25 | export interface CreateProjectOptions { 26 | tsConfigFilePath: string; 27 | } 28 | 29 | export interface Project { 30 | ts: TypeScript; 31 | languageService: LanguageService; 32 | program: Program; 33 | } 34 | 35 | export function createProject(options: CreateProjectOptions, changedFiles: Map): Project | undefined { 36 | function readFile(path:string,): string|undefined { 37 | if (changedFiles.get(path) !== undefined ){ 38 | return changedFiles.get(path)?.newText; 39 | } 40 | return ts.sys.readFile(path); 41 | } 42 | 43 | const ts = loadTypeScript(); 44 | const parseConfigHost: ParseConfigFileHost = { 45 | fileExists: ts.sys.fileExists, 46 | getCurrentDirectory: ts.sys.getCurrentDirectory, 47 | readDirectory: ts.sys.readDirectory, 48 | readFile: readFile, 49 | useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, 50 | onUnRecoverableConfigFileDiagnostic: diagnostic => { 51 | const message = ts.formatDiagnosticsWithColorAndContext([diagnostic], { 52 | getCanonicalFileName: fileName => fileName, 53 | getCurrentDirectory: ts.sys.getCurrentDirectory, 54 | getNewLine: () => ts.sys.newLine 55 | }); 56 | // TODO: let CLI choose how to handle error instead of throwing here 57 | throw new Error(message); 58 | } 59 | } 60 | 61 | const commandLine = ts.getParsedCommandLineOfConfigFile(path.resolve(options.tsConfigFilePath), undefined, parseConfigHost); 62 | if (!commandLine) return undefined; 63 | 64 | const languageServiceHost: LanguageServiceHost = { 65 | getCompilationSettings: () => commandLine.options, 66 | getProjectReferences: () => commandLine.projectReferences, 67 | getCurrentDirectory: ts.sys.getCurrentDirectory, 68 | getDefaultLibFileName: options => path.join(path.dirname(ts.sys.getExecutingFilePath()), ts.getDefaultLibFileName(options)), 69 | fileExists: ts.sys.fileExists, 70 | readFile: readFile, 71 | readDirectory: ts.sys.readDirectory, 72 | directoryExists: ts.sys.directoryExists, 73 | getDirectories: ts.sys.getDirectories, 74 | getScriptFileNames: () => commandLine.fileNames, 75 | getScriptVersion: () => "0", // Get a new project if files change 76 | getScriptSnapshot: fileName => { 77 | const fileContents = readFile(fileName); 78 | if (fileContents === undefined){ 79 | return undefined 80 | } 81 | return ts.ScriptSnapshot.fromString(fileContents); 82 | }, 83 | }; 84 | 85 | const languageService = ts.createLanguageService(languageServiceHost); 86 | const program = languageService.getProgram(); 87 | if (!program) return undefined; 88 | 89 | return { ts, languageService, program }; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, getLineAndCharacterOfPosition, getPositionOfLineAndCharacter, SourceFile } from "typescript"; 2 | import { ChangeDiagnostic, FixAndDiagnostic, Host } from "."; 3 | 4 | const resetEscapeSequence = "\u001b[0m"; 5 | const urlSchemeSeparator = "://"; 6 | const altDirectorySeparator = "\\"; 7 | const directorySeparator = "/"; 8 | const backslashRegExp = /\\/g; 9 | const relativePathSegmentRegExp = /(?:\/\/)|(?:^|\/)\.\.?(?:$|\/)/; 10 | const ellipsis = "..."; 11 | const halfIndent = " "; 12 | const indent = " "; 13 | const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g; 14 | const gutterStyleSequence = "\u001b[7m"; 15 | const gutterSeparator = " "; 16 | 17 | enum ForegroundColorEscapeSequences { 18 | Grey = "\u001b[90m", 19 | Red = "\u001b[91m", 20 | Yellow = "\u001b[93m", 21 | Blue = "\u001b[94m", 22 | Cyan = "\u001b[96m" 23 | } 24 | 25 | const enum CharacterCodes { 26 | nullCharacter = 0, 27 | maxAsciiCharacter = 0x7F, 28 | 29 | lineFeed = 0x0A, // \n 30 | carriageReturn = 0x0D, // \r 31 | lineSeparator = 0x2028, 32 | paragraphSeparator = 0x2029, 33 | nextLine = 0x0085, 34 | 35 | // Unicode 3.0 space characters 36 | space = 0x0020, // " " 37 | nonBreakingSpace = 0x00A0, // 38 | enQuad = 0x2000, 39 | emQuad = 0x2001, 40 | enSpace = 0x2002, 41 | emSpace = 0x2003, 42 | threePerEmSpace = 0x2004, 43 | fourPerEmSpace = 0x2005, 44 | sixPerEmSpace = 0x2006, 45 | figureSpace = 0x2007, 46 | punctuationSpace = 0x2008, 47 | thinSpace = 0x2009, 48 | hairSpace = 0x200A, 49 | zeroWidthSpace = 0x200B, 50 | narrowNoBreakSpace = 0x202F, 51 | ideographicSpace = 0x3000, 52 | mathematicalSpace = 0x205F, 53 | ogham = 0x1680, 54 | 55 | _ = 0x5F, 56 | $ = 0x24, 57 | 58 | _0 = 0x30, 59 | _1 = 0x31, 60 | _2 = 0x32, 61 | _3 = 0x33, 62 | _4 = 0x34, 63 | _5 = 0x35, 64 | _6 = 0x36, 65 | _7 = 0x37, 66 | _8 = 0x38, 67 | _9 = 0x39, 68 | 69 | a = 0x61, 70 | b = 0x62, 71 | c = 0x63, 72 | d = 0x64, 73 | e = 0x65, 74 | f = 0x66, 75 | g = 0x67, 76 | h = 0x68, 77 | i = 0x69, 78 | j = 0x6A, 79 | k = 0x6B, 80 | l = 0x6C, 81 | m = 0x6D, 82 | n = 0x6E, 83 | o = 0x6F, 84 | p = 0x70, 85 | q = 0x71, 86 | r = 0x72, 87 | s = 0x73, 88 | t = 0x74, 89 | u = 0x75, 90 | v = 0x76, 91 | w = 0x77, 92 | x = 0x78, 93 | y = 0x79, 94 | z = 0x7A, 95 | 96 | A = 0x41, 97 | B = 0x42, 98 | C = 0x43, 99 | D = 0x44, 100 | E = 0x45, 101 | F = 0x46, 102 | G = 0x47, 103 | H = 0x48, 104 | I = 0x49, 105 | J = 0x4A, 106 | K = 0x4B, 107 | L = 0x4C, 108 | M = 0x4D, 109 | N = 0x4E, 110 | O = 0x4F, 111 | P = 0x50, 112 | Q = 0x51, 113 | R = 0x52, 114 | S = 0x53, 115 | T = 0x54, 116 | U = 0x55, 117 | V = 0x56, 118 | W = 0x57, 119 | X = 0x58, 120 | Y = 0x59, 121 | Z = 0x5a, 122 | 123 | ampersand = 0x26, // & 124 | asterisk = 0x2A, // * 125 | at = 0x40, // @ 126 | backslash = 0x5C, // \ 127 | backtick = 0x60, // ` 128 | bar = 0x7C, // | 129 | caret = 0x5E, // ^ 130 | closeBrace = 0x7D, // } 131 | closeBracket = 0x5D, // ] 132 | closeParen = 0x29, // ) 133 | colon = 0x3A, // : 134 | comma = 0x2C, // , 135 | dot = 0x2E, // . 136 | doubleQuote = 0x22, // " 137 | equals = 0x3D, // = 138 | exclamation = 0x21, // ! 139 | greaterThan = 0x3E, // > 140 | hash = 0x23, // # 141 | lessThan = 0x3C, // < 142 | minus = 0x2D, // - 143 | openBrace = 0x7B, // { 144 | openBracket = 0x5B, // [ 145 | openParen = 0x28, // ( 146 | percent = 0x25, // % 147 | plus = 0x2B, // + 148 | question = 0x3F, // ? 149 | semicolon = 0x3B, // ; 150 | singleQuote = 0x27, // ' 151 | slash = 0x2F, // / 152 | tilde = 0x7E, // ~ 153 | 154 | backspace = 0x08, // \b 155 | formFeed = 0x0C, // \f 156 | byteOrderMark = 0xFEFF, 157 | tab = 0x09, // \t 158 | verticalTab = 0x0B, // \v 159 | } 160 | 161 | function formatColorAndReset(text: string, formatStyle: string) { 162 | return formatStyle + text + resetEscapeSequence; 163 | } 164 | 165 | function isVolumeCharacter(charCode: number) { 166 | return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) || 167 | (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z); 168 | } 169 | 170 | function getFileUrlVolumeSeparatorEnd(url: string, start: number) { 171 | const ch0 = url.charCodeAt(start); 172 | if (ch0 === CharacterCodes.colon) return start + 1; 173 | if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) { 174 | const ch2 = url.charCodeAt(start + 2); 175 | if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) return start + 3; 176 | } 177 | return -1; 178 | } 179 | 180 | function getEncodedRootLength(path: string): number { 181 | if (!path) return 0; 182 | const ch0 = path.charCodeAt(0); 183 | 184 | // POSIX or UNC 185 | if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) { 186 | if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\") 187 | 188 | const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2); 189 | if (p1 < 0) return path.length; // UNC: "//server" or "\\server" 190 | 191 | return p1 + 1; // UNC: "//server/" or "\\server\" 192 | } 193 | 194 | // DOS 195 | if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) { 196 | const ch2 = path.charCodeAt(2); 197 | if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) return 3; // DOS: "c:/" or "c:\" 198 | if (path.length === 2) return 2; // DOS: "c:" (but not "c:d") 199 | } 200 | 201 | // URL 202 | const schemeEnd = path.indexOf(urlSchemeSeparator); 203 | if (schemeEnd !== -1) { 204 | const authorityStart = schemeEnd + urlSchemeSeparator.length; 205 | const authorityEnd = path.indexOf(directorySeparator, authorityStart); 206 | if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path" 207 | // For local "file" URLs, include the leading DOS volume (if present). 208 | // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a 209 | // special case interpreted as "the machine from which the URL is being interpreted". 210 | const scheme = path.slice(0, schemeEnd); 211 | const authority = path.slice(authorityStart, authorityEnd); 212 | if (scheme === "file" && (authority === "" || authority === "localhost") && 213 | isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) { 214 | const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); 215 | if (volumeSeparatorEnd !== -1) { 216 | if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) { 217 | // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/" 218 | return ~(volumeSeparatorEnd + 1); 219 | } 220 | if (volumeSeparatorEnd === path.length) { 221 | // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a" 222 | // but not "file:///c:d" or "file:///c%3ad" 223 | return ~volumeSeparatorEnd; 224 | } 225 | } 226 | } 227 | return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" 228 | } 229 | return ~path.length; // URL: "file://server", "http://server" 230 | } 231 | 232 | // relative 233 | return 0; 234 | } 235 | 236 | function isRootedDiskPath(path: string) { 237 | return getEncodedRootLength(path) > 0; 238 | } 239 | 240 | function isAnyDirectorySeparator(charCode: number): boolean { 241 | return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; 242 | } 243 | 244 | function hasTrailingDirectorySeparator(path: string) { 245 | return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); 246 | } 247 | 248 | function ensureTrailingDirectorySeparator(path: string) { 249 | if (!hasTrailingDirectorySeparator(path)) { 250 | return path + directorySeparator; 251 | } 252 | 253 | return path; 254 | } 255 | 256 | function getPathFromPathComponents(pathComponents: readonly string[]) { 257 | if (pathComponents.length === 0) return ""; 258 | 259 | const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); 260 | return root + pathComponents.slice(1).join(directorySeparator); 261 | } 262 | 263 | function some(array: readonly T[] | undefined, predicate?: (value: T) => boolean): boolean { 264 | if (array) { 265 | if (predicate) { 266 | for (const v of array) { 267 | if (predicate(v)) { 268 | return true; 269 | } 270 | } 271 | } 272 | else { 273 | return array.length > 0; 274 | } 275 | } 276 | return false; 277 | } 278 | 279 | function reducePathComponents(components: readonly string[]) { 280 | if (!some(components)) return []; 281 | const reduced = [components[0]]; 282 | for (let i = 1; i < components.length; i++) { 283 | const component = components[i]; 284 | if (!component) continue; 285 | if (component === ".") continue; 286 | if (component === "..") { 287 | if (reduced.length > 1) { 288 | if (reduced[reduced.length - 1] !== "..") { 289 | reduced.pop(); 290 | continue; 291 | } 292 | } 293 | else if (reduced[0]) continue; 294 | } 295 | reduced.push(component); 296 | } 297 | return reduced; 298 | } 299 | 300 | function normalizeSlashes(path: string): string { 301 | const index = path.indexOf("\\"); 302 | if (index === -1) { 303 | return path; 304 | } 305 | backslashRegExp.lastIndex = index; // prime regex with known position 306 | return path.replace(backslashRegExp, directorySeparator); 307 | } 308 | 309 | function combinePaths(path: string, ...paths: (string | undefined)[]): string { 310 | if (path) path = normalizeSlashes(path); 311 | for (let relativePath of paths) { 312 | if (!relativePath) continue; 313 | relativePath = normalizeSlashes(relativePath); 314 | if (!path || getRootLength(relativePath) !== 0) { 315 | path = relativePath; 316 | } 317 | else { 318 | path = ensureTrailingDirectorySeparator(path) + relativePath; 319 | } 320 | } 321 | return path; 322 | } 323 | 324 | function getRootLength(path: string) { 325 | const rootLength = getEncodedRootLength(path); 326 | return rootLength < 0 ? ~rootLength : rootLength; 327 | } 328 | 329 | function lastOrUndefined(array: readonly T[] | undefined): T | undefined { 330 | return array === undefined || array.length === 0 ? undefined : array[array.length - 1]; 331 | } 332 | 333 | function pathComponents(path: string, rootLength: number) { 334 | const root = path.substring(0, rootLength); 335 | const rest = path.substring(rootLength).split(directorySeparator); 336 | if (rest.length && !lastOrUndefined(rest)) rest.pop(); 337 | return [root, ...rest]; 338 | } 339 | 340 | function getPathComponents(path: string, currentDirectory = "") { 341 | path = combinePaths(currentDirectory, path); 342 | return pathComponents(path, getRootLength(path)); 343 | } 344 | 345 | function equateStringsCaseInsensitive(a: string, b: string) { 346 | return a === b 347 | || a !== undefined 348 | && b !== undefined 349 | && a.toUpperCase() === b.toUpperCase(); 350 | } 351 | 352 | function identity(x: T) { 353 | return x; 354 | } 355 | 356 | function toLowerCase(x: string) { 357 | return x.toLowerCase(); 358 | } 359 | 360 | function toFileNameLowerCase(x: string) { 361 | return fileNameLowerCaseRegExp.test(x) ? 362 | x.replace(fileNameLowerCaseRegExp, toLowerCase) : 363 | x; 364 | } 365 | 366 | export type GetCanonicalFileName = (fileName: string) => string; 367 | export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { 368 | return useCaseSensitiveFileNames ? identity : toFileNameLowerCase; 369 | } 370 | 371 | function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { 372 | const fromComponents = reducePathComponents(getPathComponents(from)); 373 | const toComponents = reducePathComponents(getPathComponents(to)); 374 | 375 | let start: number; 376 | for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { 377 | const fromComponent = getCanonicalFileName(fromComponents[start]); 378 | const toComponent = getCanonicalFileName(toComponents[start]); 379 | const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer; 380 | if (!comparer(fromComponent, toComponent)) break; 381 | } 382 | 383 | if (start === 0) { 384 | return toComponents; 385 | } 386 | 387 | const components = toComponents.slice(start); 388 | const relative: string[] = []; 389 | for (; start < fromComponents.length; start++) { 390 | relative.push(".."); 391 | } 392 | return ["", ...relative, ...components]; 393 | } 394 | 395 | function normalizePath(path: string): string { 396 | path = normalizeSlashes(path); 397 | // Most paths don't require normalization 398 | if (!relativePathSegmentRegExp.test(path)) { 399 | return path; 400 | } 401 | // Some paths only require cleanup of `/./` or leading `./` 402 | const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, ""); 403 | if (simplified !== path) { 404 | path = simplified; 405 | if (!relativePathSegmentRegExp.test(path)) { 406 | return path; 407 | } 408 | } 409 | // Other paths require full normalization 410 | const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path))); 411 | return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; 412 | } 413 | 414 | function resolvePath(path: string, ...paths: (string | undefined)[]): string { 415 | return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path)); 416 | } 417 | 418 | function equateValues(a: T, b: T) { 419 | return a === b; 420 | } 421 | 422 | function equateStringsCaseSensitive(a: string, b: string) { 423 | return equateValues(a, b); 424 | } 425 | 426 | function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { 427 | const pathComponents = getPathComponentsRelativeTo( 428 | resolvePath(currentDirectory, directoryPathOrUrl), 429 | resolvePath(currentDirectory, relativeOrAbsolutePath), 430 | equateStringsCaseSensitive, 431 | getCanonicalFileName 432 | ); 433 | 434 | const firstComponent = pathComponents[0]; 435 | if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { 436 | const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; 437 | pathComponents[0] = prefix + firstComponent; 438 | } 439 | 440 | return getPathFromPathComponents(pathComponents); 441 | } 442 | 443 | function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { 444 | return !isRootedDiskPath(absoluteOrRelativePath) 445 | ? absoluteOrRelativePath 446 | : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); 447 | } 448 | 449 | function formatLocation(file: SourceFile, start: number, host: Host, color = formatColorAndReset) { 450 | const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); 451 | const relativeFileName = host ? convertToRelativePath(file.fileName, host.getCurrentDirectory(), (fileName: string) => host.getCanonicalFileName(fileName)) : file.fileName; 452 | 453 | let output = ""; 454 | output += color(relativeFileName, ForegroundColorEscapeSequences.Cyan); 455 | output += ":"; 456 | output += color(`${firstLine + 1}`, ForegroundColorEscapeSequences.Yellow); 457 | output += ":"; 458 | output += color(`${firstLineChar + 1}`, ForegroundColorEscapeSequences.Yellow); 459 | return output; 460 | } 461 | 462 | enum DiagnosticCategory { 463 | Warning, 464 | Error, 465 | Suggestion, 466 | Message 467 | } 468 | 469 | function diagnosticCategoryName(d: { category: DiagnosticCategory }, lowerCase = true): string { 470 | const name = DiagnosticCategory[d.category]; 471 | return lowerCase ? name.toLowerCase() : name; 472 | } 473 | 474 | interface DiagnosticMessageChain { 475 | messageText: string; 476 | category: DiagnosticCategory; 477 | code: number; 478 | next?: DiagnosticMessageChain[]; 479 | } 480 | 481 | function isString(text: unknown): text is string { 482 | return typeof text === "string"; 483 | } 484 | 485 | function flattenDiagnosticMessageText(diag: string | DiagnosticMessageChain | undefined, newLine: string, indent = 0): string { 486 | if (isString(diag)) { 487 | return diag; 488 | } 489 | else if (diag === undefined) { 490 | return ""; 491 | } 492 | let result = ""; 493 | if (indent) { 494 | result += newLine; 495 | 496 | for (let i = 0; i < indent; i++) { 497 | result += " "; 498 | } 499 | } 500 | result += diag.messageText; 501 | indent++; 502 | if (diag.next) { 503 | for (const kid of diag.next) { 504 | result += flattenDiagnosticMessageText(kid, newLine, indent); 505 | } 506 | } 507 | return result; 508 | } 509 | 510 | function padLeft(s: string, length: number, padString: " " | "0" = " ") { 511 | return length <= s.length ? s : padString.repeat(length - s.length) + s; 512 | } 513 | 514 | function isWhiteSpaceSingleLine(ch: number): boolean { 515 | // Note: nextLine is in the Zs space, and should be considered to be a whitespace. 516 | // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript. 517 | return ch === CharacterCodes.space || 518 | ch === CharacterCodes.tab || 519 | ch === CharacterCodes.verticalTab || 520 | ch === CharacterCodes.formFeed || 521 | ch === CharacterCodes.nonBreakingSpace || 522 | ch === CharacterCodes.nextLine || 523 | ch === CharacterCodes.ogham || 524 | ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace || 525 | ch === CharacterCodes.narrowNoBreakSpace || 526 | ch === CharacterCodes.mathematicalSpace || 527 | ch === CharacterCodes.ideographicSpace || 528 | ch === CharacterCodes.byteOrderMark; 529 | } 530 | 531 | function isLineBreak(ch: number): boolean { 532 | // ES5 7.3: 533 | // The ECMAScript line terminator characters are listed in Table 3. 534 | // Table 3: Line Terminator Characters 535 | // Code Unit Value Name Formal Name 536 | // \u000A Line Feed 537 | // \u000D Carriage Return 538 | // \u2028 Line separator 539 | // \u2029 Paragraph separator 540 | // Only the characters in Table 3 are treated as line terminators. Other new line or line 541 | // breaking characters are treated as white space but not as line terminators. 542 | 543 | return ch === CharacterCodes.lineFeed || 544 | ch === CharacterCodes.carriageReturn || 545 | ch === CharacterCodes.lineSeparator || 546 | ch === CharacterCodes.paragraphSeparator; 547 | } 548 | 549 | function isWhiteSpaceLike(ch: number): boolean { 550 | return isWhiteSpaceSingleLine(ch) || isLineBreak(ch); 551 | } 552 | 553 | function trimEndImpl(s: string) { 554 | let end = s.length - 1; 555 | while (end >= 0) { 556 | if (!isWhiteSpaceLike(s.charCodeAt(end))) break; 557 | end--; 558 | } 559 | return s.slice(0, end + 1); 560 | } 561 | 562 | const trimStringEnd = !!String.prototype.trimEnd ? ((s: string) => s.trimEnd()) : trimEndImpl; 563 | 564 | function formatCodeSpan(file: SourceFile, start: number, length: number, indent: string, host: Host) { 565 | const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); 566 | const { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start + length); 567 | const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line; 568 | 569 | const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; 570 | let gutterWidth = (lastLine + 1 + "").length; 571 | if (hasMoreThanFiveLines) { 572 | gutterWidth = Math.max(ellipsis.length, gutterWidth); 573 | } 574 | 575 | let context = ""; 576 | for (let i = firstLine; i <= lastLine; i++) { 577 | context += host.getNewLine(); 578 | // If the error spans over 5 lines, we'll only show the first 2 and last 2 lines, 579 | // so we'll skip ahead to the second-to-last line. 580 | if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) { 581 | context += indent + formatColorAndReset(padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); 582 | i = lastLine - 1; 583 | } 584 | 585 | const lineStart = getPositionOfLineAndCharacter(file, i, 0); 586 | const lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; 587 | let lineContent = file.text.slice(lineStart, lineEnd); 588 | lineContent = trimStringEnd(lineContent); // trim from end 589 | lineContent = lineContent.replace(/\t/g, " "); // convert tabs to single spaces 590 | 591 | // Output the gutter and the actual contents of the line. 592 | context += indent + formatColorAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; 593 | context += lineContent + host.getNewLine(); 594 | 595 | // Output the gutter and the error span for the line using tildes. 596 | context += indent + formatColorAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator; 597 | // context += squiggleColor; 598 | // if (i === firstLine) { 599 | // // If we're on the last line, then limit it to the last character of the last line. 600 | // // Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position. 601 | // const lastCharForLine = i === lastLine ? lastLineChar : undefined; 602 | 603 | // context += lineContent.slice(0, firstLineChar).replace(/\S/g, " "); 604 | // context += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~"); 605 | // } 606 | // else if (i === lastLine) { 607 | // context += lineContent.slice(0, lastLineChar).replace(/./g, "~"); 608 | // } 609 | // else { 610 | // // Squiggle the entire line. 611 | // context += lineContent.replace(/./g, "~"); 612 | // } 613 | context += resetEscapeSequence; 614 | } 615 | return context; 616 | } 617 | 618 | function getCategoryFormat(category: DiagnosticCategory): ForegroundColorEscapeSequences { 619 | switch (category) { 620 | case DiagnosticCategory.Error: return ForegroundColorEscapeSequences.Red; 621 | case DiagnosticCategory.Warning: return ForegroundColorEscapeSequences.Yellow; 622 | // case DiagnosticCategory.Suggestion: return Debug.fail("Should never get an Info diagnostic on the command line."); 623 | case DiagnosticCategory.Suggestion: return ForegroundColorEscapeSequences.Cyan; 624 | case DiagnosticCategory.Message: return ForegroundColorEscapeSequences.Blue; 625 | } 626 | } 627 | 628 | export function formatFixOnADifferentLocation(diagnostics: readonly ChangeDiagnostic[], host: Host): string { 629 | let output = ""; 630 | for (const diagnostic of diagnostics) { 631 | if (diagnostic.file) { 632 | const { file, start } = diagnostic; 633 | output += formatLocation(file, start!, host); 634 | } 635 | 636 | if (diagnostic.file) { 637 | output += host.getNewLine(); 638 | output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", host); 639 | } 640 | 641 | output += host.getNewLine(); 642 | } 643 | return output; 644 | } 645 | 646 | export function formatDiagnosticsWithColorAndContextTsFix(diagnostics: readonly Diagnostic[], host: Host): string { 647 | let output = ""; 648 | for (const diagnostic of diagnostics) { 649 | if (diagnostic.file) { 650 | const { file, start } = diagnostic; 651 | output += formatLocation(file, start!, host); 652 | output += " - "; 653 | } 654 | 655 | output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category)); 656 | output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); 657 | output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); 658 | output += host.getNewLine(); 659 | } 660 | 661 | if (diagnostics[0].file) { 662 | output += formatCodeSpan(diagnostics[0].file, diagnostics[0].start!, diagnostics[0].length!, "", host); 663 | } 664 | 665 | output += host.getNewLine(); 666 | 667 | return output; 668 | } 669 | 670 | function formatCodeSpanForFixesInTheSameSpan(file: SourceFile, start: number, length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, host: Host) { 671 | const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); 672 | const { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start + length); 673 | const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line; 674 | 675 | const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; 676 | let gutterWidth = (lastLine + 1 + "").length; 677 | if (hasMoreThanFiveLines) { 678 | gutterWidth = Math.max(ellipsis.length, gutterWidth); 679 | } 680 | 681 | let context = ""; 682 | for (let i = firstLine; i <= lastLine; i++) { 683 | context += host.getNewLine(); 684 | // If the error spans over 5 lines, we'll only show the first 2 and last 2 lines, 685 | // so we'll skip ahead to the second-to-last line. 686 | if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) { 687 | context += indent + formatColorAndReset(padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); 688 | i = lastLine - 1; 689 | } 690 | 691 | const lineStart = getPositionOfLineAndCharacter(file, i, 0); 692 | const lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; 693 | let lineContent = file.text.slice(lineStart, lineEnd); 694 | lineContent = trimStringEnd(lineContent); // trim from end 695 | lineContent = lineContent.replace(/\t/g, " "); // convert tabs to single spaces 696 | 697 | // Output the gutter and the actual contents of the line. 698 | context += indent + formatColorAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; 699 | context += lineContent + host.getNewLine(); 700 | 701 | // Output the gutter and the error span for the line using tildes. 702 | context += indent + formatColorAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator; 703 | context += squiggleColor; 704 | if (i === firstLine) { 705 | // If we're on the last line, then limit it to the last character of the last line. 706 | // Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position. 707 | const lastCharForLine = i === lastLine ? lastLineChar : undefined; 708 | 709 | context += lineContent.slice(0, firstLineChar).replace(/\S/g, " "); 710 | context += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~"); 711 | } 712 | else if (i === lastLine) { 713 | context += lineContent.slice(0, lastLineChar).replace(/./g, "~"); 714 | } 715 | else { 716 | // Squiggle the entire line. 717 | context += lineContent.replace(/./g, "~"); 718 | } 719 | context += resetEscapeSequence; 720 | } 721 | return context; 722 | } 723 | 724 | export function formatFixesInTheSameSpan(fixAndDiagnostics: FixAndDiagnostic[], host: Host): string { 725 | let output = ""; 726 | for (const fixAndDiagnostic of fixAndDiagnostics) { 727 | if (fixAndDiagnostic.diagnostic.file) { 728 | const { file, start } = fixAndDiagnostic.diagnostic; 729 | output += formatLocation(file, start!, host); 730 | output += " - "; 731 | } 732 | 733 | output += formatColorAndReset(diagnosticCategoryName(fixAndDiagnostic.diagnostic), getCategoryFormat(fixAndDiagnostic.diagnostic.category)); 734 | output += formatColorAndReset(` TS${fixAndDiagnostic.diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); 735 | output += flattenDiagnosticMessageText(fixAndDiagnostic.diagnostic.messageText, host.getNewLine()); 736 | output += host.getNewLine(); 737 | if (fixAndDiagnostic.diagnostic.file) { 738 | output += formatCodeSpanForFixesInTheSameSpan(fixAndDiagnostic.diagnostic.file, fixAndDiagnostic.diagnostic.start!, fixAndDiagnostic.diagnostic.length!, "", getCategoryFormat(fixAndDiagnostic.diagnostic.category), host); 739 | output += host.getNewLine(); 740 | } 741 | } 742 | 743 | output += host.getNewLine(); 744 | 745 | return output; 746 | } -------------------------------------------------------------------------------- /test/integration/__snapshots__/addMissingMember.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addMissingMember", 3 | "args": [ 4 | "-e", 5 | "2339", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 2 diagnostics with code 2339", 13 | "Found 4 codefixes", 14 | "Fixes to be applied: 0\r\nNo applied fixes: 4\r\n", 15 | "No changes remaining for ts-fix", 16 | "\r\nThe fixes not applied by ts-fix for this project are:", 17 | "fixMissingMember types.ts", 18 | "\r\nNo changes made in any files" 19 | ], 20 | "remainingChanges": [], 21 | "filesWritten": { 22 | "dataType": "Map", 23 | "value": [] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/addMissingOverride.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addMissingOverride", 3 | "args": [ 4 | "-e", 5 | "4114", 6 | "-f", 7 | "fixOverrideModifier", 8 | "-w", 9 | "--ignoreGitStatus" 10 | ], 11 | "logs": [ 12 | "The project is being created...\r\n", 13 | "Using TypeScript 5.5.3", 14 | "\r\nFound 2 diagnostics with code 4114", 15 | "Found 2 codefixes with name fixOverrideModifier", 16 | "Fixes to be applied: 2\r\nNo applied fixes: 0\r\n", 17 | "\r\nNo diagnostics found with code 4114", 18 | "No codefixes found with name fixOverrideModifier", 19 | "No changes remaining for ts-fix", 20 | "\r\nChanges were made in the following files:", 21 | "Updated index.ts" 22 | ], 23 | "remainingChanges": [], 24 | "filesWritten": { 25 | "dataType": "Map", 26 | "value": [ 27 | [ 28 | "index.ts", 29 | "class Base {\n m() {}\n}\n\nclass Derived extends Base {\n override m() {}\n}\n\nclass MoreDerived extends Derived {\n override m() {}\n}\n" 30 | ] 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/addMissingOverrideByError.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addMissingOverrideByError", 3 | "args": [ 4 | "-e", 5 | "4114", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 2 diagnostics with code 4114", 13 | "Found 2 codefixes", 14 | "Fixes to be applied: 2\r\nNo applied fixes: 0\r\n", 15 | "\r\nNo diagnostics found with code 4114", 16 | "Found 0 codefixes", 17 | "No changes remaining for ts-fix", 18 | "\r\nChanges were made in the following files:", 19 | "Updated index.ts" 20 | ], 21 | "remainingChanges": [], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [ 25 | [ 26 | "index.ts", 27 | "class Base {\n m() {}\n}\n\nclass Derived extends Base {\n override m() {}\n}\n\nclass MoreDerived extends Derived {\n override m() {}\n}\n" 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/addMissingOverrideByFixName.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addMissingOverrideByFixName", 3 | "args": [ 4 | "-f", 5 | "fixOverrideModifier", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 2 diagnostics in 8 files", 13 | "Found 2 codefixes with name fixOverrideModifier", 14 | "Fixes to be applied: 2\r\nNo applied fixes: 0\r\n", 15 | "\r\nFound 0 diagnostics in 8 files", 16 | "No codefixes found with name fixOverrideModifier", 17 | "No changes remaining for ts-fix", 18 | "\r\nChanges were made in the following files:", 19 | "Updated index.ts" 20 | ], 21 | "remainingChanges": [], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [ 25 | [ 26 | "index.ts", 27 | "class Base {\n m() {}\n}\n\nclass Derived extends Base {\n override m() {}\n}\n\nclass MoreDerived extends Derived {\n override m() {}\n}\n" 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/addOneUnknown.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addOneUnknown", 3 | "args": [ 4 | "-e", 5 | "2352", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 1 diagnostics with code 2352", 13 | "Found 1 codefixes", 14 | "Fixes to be applied: 1\r\nNo applied fixes: 0\r\n", 15 | "\r\nNo diagnostics found with code 2352", 16 | "Found 0 codefixes", 17 | "No changes remaining for ts-fix", 18 | "\r\nChanges were made in the following files:", 19 | "Updated index.ts" 20 | ], 21 | "remainingChanges": [], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [ 25 | [ 26 | "index.ts", 27 | "\"words\";" 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/addOneUnknownToList.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addOneUnknownToList", 3 | "args": [ 4 | "-e", 5 | "2352", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 1 diagnostics with code 2352", 13 | "Found 1 codefixes", 14 | "Fixes to be applied: 1\r\nNo applied fixes: 0\r\n", 15 | "\r\nNo diagnostics found with code 2352", 16 | "Found 0 codefixes", 17 | "No changes remaining for ts-fix", 18 | "\r\nChanges were made in the following files:", 19 | "Updated index.ts" 20 | ], 21 | "remainingChanges": [], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [ 25 | [ 26 | "index.ts", 27 | "[\"words\"];\n" 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/addThreeUnknown.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/addThreeUnknown", 3 | "args": [ 4 | "-e", 5 | "2352", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 3 diagnostics with code 2352", 13 | "Found 3 codefixes", 14 | "Fixes to be applied: 3\r\nNo applied fixes: 0\r\n", 15 | "\r\nNo diagnostics found with code 2352", 16 | "Found 0 codefixes", 17 | "No changes remaining for ts-fix", 18 | "\r\nChanges were made in the following files:", 19 | "Updated index.ts" 20 | ], 21 | "remainingChanges": [], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [ 25 | [ 26 | "index.ts", 27 | "[\"words\"];\n\n\"words\";\n\n0 * (4 + 3) / 100;" 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/multipleFixesInSameLine.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/multipleFixesInSameLine", 3 | "args": [ 4 | "-e", 5 | "7006", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nFound 2 diagnostics with code 7006", 13 | "Found 2 codefixes", 14 | "Fixes to be applied: 2\r\nNo applied fixes: 0\r\n", 15 | "\r\nNo diagnostics found with code 7006", 16 | "Found 0 codefixes", 17 | "No changes remaining for ts-fix", 18 | "\r\nChanges were made in the following files:", 19 | "Updated index.ts" 20 | ], 21 | "remainingChanges": [], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [ 25 | [ 26 | "index.ts", 27 | "function (a: any, b: any) {\n return a + b;\n}" 28 | ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/noDiagnostics.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/noDiagnostics", 3 | "args": [ 4 | "-e", 5 | "4114", 6 | "-w", 7 | "--ignoreGitStatus" 8 | ], 9 | "logs": [ 10 | "The project is being created...\r\n", 11 | "Using TypeScript 5.5.3", 12 | "\r\nNo diagnostics found with code 4114", 13 | "Found 0 codefixes", 14 | "No changes remaining for ts-fix", 15 | "\r\nNo changes made in any files" 16 | ], 17 | "remainingChanges": [], 18 | "filesWritten": { 19 | "dataType": "Map", 20 | "value": [] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/twoErrorCodes.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/twoErrorCodes", 3 | "args": [ 4 | "-e", 5 | "4114", 6 | "2352", 7 | "-w", 8 | "--ignoreGitStatus" 9 | ], 10 | "logs": [ 11 | "The project is being created...\r\n", 12 | "Using TypeScript 5.5.3", 13 | "\r\nFound 2 diagnostics with code 4114", 14 | "\r\nFound 3 diagnostics with code 2352", 15 | "Found 5 codefixes", 16 | "Fixes to be applied: 5\r\nNo applied fixes: 0\r\n", 17 | "\r\nNo diagnostics found with code 4114", 18 | "\r\nNo diagnostics found with code 2352", 19 | "Found 0 codefixes", 20 | "No changes remaining for ts-fix", 21 | "\r\nChanges were made in the following files:", 22 | "Updated addoverrides.ts", 23 | "Updated addunknowns.ts" 24 | ], 25 | "remainingChanges": [], 26 | "filesWritten": { 27 | "dataType": "Map", 28 | "value": [ 29 | [ 30 | "addoverrides.ts", 31 | "class Base {\n m() {}\n}\n\nclass Derived extends Base {\n override m() {}\n}\n\nclass MoreDerived extends Derived {\n override m() {}\n}\n" 32 | ], 33 | [ 34 | "addunknowns.ts", 35 | "[\"words\"];\n\n\"words\";\n\n0 * (4 + 3) / 100;" 36 | ] 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/twoFixNames.shot: -------------------------------------------------------------------------------- 1 | { 2 | "cwd": "cases/twoFixNames", 3 | "args": [ 4 | "-f", 5 | "fixOverrideModifier", 6 | "addConvertToUnknownForNonOverlappingTypes", 7 | "-w", 8 | "--ignoreGitStatus" 9 | ], 10 | "logs": [ 11 | "The project is being created...\r\n", 12 | "Using TypeScript 5.5.3", 13 | "\r\nFound 6 diagnostics in 9 files", 14 | "Found 2 codefixes with name fixOverrideModifier", 15 | "Found 3 codefixes with name addConvertToUnknownForNonOverlappingTypes", 16 | "Fixes to be applied: 5\r\nNo applied fixes: 0\r\n", 17 | "\r\nFound 1 diagnostics in 9 files", 18 | "No codefixes found with name fixOverrideModifier", 19 | "No codefixes found with name addConvertToUnknownForNonOverlappingTypes", 20 | "No changes remaining for ts-fix", 21 | "\r\nChanges were made in the following files:", 22 | "Updated addoverrides.ts", 23 | "Updated addunknowns.ts" 24 | ], 25 | "remainingChanges": [], 26 | "filesWritten": { 27 | "dataType": "Map", 28 | "value": [ 29 | [ 30 | "addoverrides.ts", 31 | "class Base {\n m() {}\n}\n\nclass Derived extends Base {\n override m() {}\n}\n\nclass MoreDerived extends Derived {\n override m() {}\n}\n" 32 | ], 33 | [ 34 | "addunknowns.ts", 35 | "[\"words\"];\n\n\"words\";\n\n0 * (4 + 3) / 100;" 36 | ] 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingMember/a.ts: -------------------------------------------------------------------------------- 1 | const i: I = { 2 | one: true, 3 | }; 4 | 5 | i.two; 6 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingMember/b.ts: -------------------------------------------------------------------------------- 1 | const i: I = { 2 | one: true, 3 | }; 4 | 5 | i.three; 6 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingMember/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 2339 -------------------------------------------------------------------------------- /test/integration/cases/addMissingMember/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingMember/types.ts: -------------------------------------------------------------------------------- 1 | interface I { 2 | one: boolean; 3 | } -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverride/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 4114 -f fixOverrideModifier -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverride/index.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | m() {} 3 | } 4 | 5 | class Derived extends Base { 6 | m() {} 7 | } 8 | 9 | class MoreDerived extends Derived { 10 | m() {} 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverride/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverrideByError/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 4114 -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverrideByError/index.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | m() {} 3 | } 4 | 5 | class Derived extends Base { 6 | m() {} 7 | } 8 | 9 | class MoreDerived extends Derived { 10 | m() {} 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverrideByError/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverrideByFixName/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -f fixOverrideModifier -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverrideByFixName/index.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | m() {} 3 | } 4 | 5 | class Derived extends Base { 6 | m() {} 7 | } 8 | 9 | class MoreDerived extends Derived { 10 | m() {} 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/cases/addMissingOverrideByFixName/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/addOneUnknown/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 2352 -------------------------------------------------------------------------------- /test/integration/cases/addOneUnknown/index.ts: -------------------------------------------------------------------------------- 1 | "words"; -------------------------------------------------------------------------------- /test/integration/cases/addOneUnknown/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/addOneUnknownToList/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 2352 -------------------------------------------------------------------------------- /test/integration/cases/addOneUnknownToList/index.ts: -------------------------------------------------------------------------------- 1 | ["words"]; 2 | -------------------------------------------------------------------------------- /test/integration/cases/addOneUnknownToList/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/addThreeUnknown/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 2352 -------------------------------------------------------------------------------- /test/integration/cases/addThreeUnknown/index.ts: -------------------------------------------------------------------------------- 1 | ["words"]; 2 | 3 | "words"; 4 | 5 | 0 * (4 + 3) / 100; -------------------------------------------------------------------------------- /test/integration/cases/addThreeUnknown/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/multipleFixesInSameLine/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 7006 -------------------------------------------------------------------------------- /test/integration/cases/multipleFixesInSameLine/index.ts: -------------------------------------------------------------------------------- 1 | function (a, b) { 2 | return a + b; 3 | } -------------------------------------------------------------------------------- /test/integration/cases/multipleFixesInSameLine/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitAny": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/noDiagnostics/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 4114 -------------------------------------------------------------------------------- /test/integration/cases/noDiagnostics/index.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | foo() { 3 | console.log("called foo in Base"); 4 | } 5 | 6 | bar() { 7 | console.log("called bar in Base"); 8 | } 9 | } -------------------------------------------------------------------------------- /test/integration/cases/noDiagnostics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/twoErrorCodes/addoverrides.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | m() {} 3 | } 4 | 5 | class Derived extends Base { 6 | m() {} 7 | } 8 | 9 | class MoreDerived extends Derived { 10 | m() {} 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/cases/twoErrorCodes/addunknowns.ts: -------------------------------------------------------------------------------- 1 | ["words"]; 2 | 3 | "words"; 4 | 5 | 0 * (4 + 3) / 100; -------------------------------------------------------------------------------- /test/integration/cases/twoErrorCodes/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 4114 2352 -------------------------------------------------------------------------------- /test/integration/cases/twoErrorCodes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/cases/twoFixNames/addoverrides.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | m() {} 3 | } 4 | 5 | class Derived extends Base { 6 | m() {} 7 | } 8 | 9 | class MoreDerived extends Derived { 10 | m() {} 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/cases/twoFixNames/addunknowns.ts: -------------------------------------------------------------------------------- 1 | ["words"]; 2 | 3 | "words"; 4 | 5 | 0 * (4 + 3) / 100; -------------------------------------------------------------------------------- /test/integration/cases/twoFixNames/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -f fixOverrideModifier addConvertToUnknownForNonOverlappingTypes -------------------------------------------------------------------------------- /test/integration/cases/twoFixNames/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [], 4 | "noImplicitOverride": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/integration.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import ts from "typescript"; 4 | import { describe, expect, test } from "vitest"; 5 | import { codefixProject } from "../../src"; 6 | import { makeOptions } from "../../src/cli"; 7 | import { normalizeLineEndings, normalizeSlashes, normalizeTextChange, TestHost } from "./testHost"; 8 | 9 | interface Snapshot { 10 | dirname: string; 11 | cwd: string; 12 | args: string[]; 13 | logs: string[]; 14 | changes: ReadonlyMap[]; 15 | filesWritten: Map; 16 | } 17 | 18 | async function baselineCLI(cwd: string, args: string[]): Promise { 19 | const host = new TestHost(cwd); 20 | const options = makeOptions(cwd, args); 21 | await codefixProject(options, host); 22 | 23 | const snapshot: Snapshot = { 24 | dirname: __dirname, 25 | cwd: normalizeSlashes(path.relative(__dirname, cwd)), 26 | args, 27 | logs: host.getLogs(), 28 | changes: host.getRemainingChanges(), 29 | filesWritten: host.getFilesWritten(), 30 | }; 31 | 32 | return snapshot; 33 | } 34 | 35 | expect.addSnapshotSerializer({ 36 | test(snapshot: Snapshot) { 37 | return !!snapshot.cwd && !!snapshot.args && !!snapshot.logs && !!snapshot.changes && !!snapshot.filesWritten; 38 | }, 39 | serialize(snapshot: Snapshot) { 40 | function replacer(_key: string, value: any) { 41 | if (value instanceof Map) { 42 | return { 43 | dataType: 'Map', 44 | value: Array.from(value.entries()).map(([fileName, value]) => { 45 | if (typeof value === "string") { 46 | return [normalizeSlashes(fileName), normalizeLineEndings(value)]; 47 | } 48 | return [normalizeSlashes(path.relative(snapshot.dirname, fileName)), normalizeTextChange(value)]; 49 | }), 50 | }; 51 | } else { 52 | return value; 53 | } 54 | } 55 | 56 | const snapshotValue = JSON.stringify({ 57 | cwd: snapshot.cwd, 58 | args: snapshot.args, 59 | logs: snapshot.logs, 60 | remainingChanges: snapshot.changes, 61 | filesWritten: snapshot.filesWritten 62 | }, replacer, 2); 63 | 64 | return snapshotValue + "\n"; 65 | } 66 | }); 67 | 68 | 69 | describe("integration tests", () => { 70 | const casesDir = path.resolve(__dirname, "cases") 71 | const cases = fs.readdirSync(casesDir); 72 | 73 | test.each(cases)("%s", async (dirName) => { 74 | const cwd = path.resolve(casesDir, dirName); 75 | 76 | const cmdFile = fs.readFileSync(path.resolve(cwd, "cmd.txt"), "utf8"); 77 | // Split cmd.txt by line, then into space-separated args, and leave off the leading `ts-fix` 78 | const commands = cmdFile.split(/\r?\n|\s/).map(c => c.trim()).filter(c => c.length > 0).slice(1).concat("-w", "--ignoreGitStatus"); 79 | 80 | const snapshot = await baselineCLI(path.posix.normalize(cwd), commands); 81 | await expect(snapshot).toMatchFileSnapshot(path.resolve(__dirname, '__snapshots__', `${dirName}.shot`)); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/integration/needFixing/addUndefinedExactOptionalPropertyTypes.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`integration tests addUndefinedExactOptionalPropertyTypes 7 1`] = ` 4 | { 5 | "cwd": "cases/addUndefinedExactOptionalPropertyTypes", 6 | "args": [ 7 | "-e", 8 | "2375", 9 | "--write" 10 | ], 11 | "logs": [ 12 | "Using TypeScript 4.3.5", 13 | "No diagnostics found with code 2375", 14 | "Found 0 codefixes" 15 | ], 16 | "changes": [ 17 | { 18 | "dataType": "Map", 19 | "value": [] 20 | } 21 | ], 22 | "filesWritten": { 23 | "dataType": "Map", 24 | "value": [] 25 | } 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /test/integration/needFixing/addUndefinedExactOptionalPropertyTypes/a.ts: -------------------------------------------------------------------------------- 1 | import { I, J } from "./types" 2 | 3 | declare var i: I 4 | declare var j: J 5 | i = j; 6 | -------------------------------------------------------------------------------- /test/integration/needFixing/addUndefinedExactOptionalPropertyTypes/b.ts: -------------------------------------------------------------------------------- 1 | import { I, J } from "./types" 2 | 3 | declare var i: I 4 | declare var j: J 5 | i = j; 6 | -------------------------------------------------------------------------------- /test/integration/needFixing/addUndefinedExactOptionalPropertyTypes/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 2375 --write -------------------------------------------------------------------------------- /test/integration/needFixing/addUndefinedExactOptionalPropertyTypes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "noEmit": true 6 | } 7 | } -------------------------------------------------------------------------------- /test/integration/needFixing/addUndefinedExactOptionalPropertyTypes/types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | name: string; 3 | email?: string; 4 | } 5 | 6 | export interface I { 7 | a?: number 8 | } 9 | export interface J { 10 | a?: number | undefined 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteUndefinedExactOptionalPropertyTypes.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`integration tests noWriteUndefinedExactOptionalPropertyTypes 10 1`] = ` 4 | { 5 | "cwd": "cases/noWriteUndefinedExactOptionalPropertyTypes", 6 | "args": [ 7 | "-e", 8 | "2375", 9 | "" 10 | ], 11 | "logs": [ 12 | "Using TypeScript 4.3.5", 13 | "No diagnostics found with code 2375", 14 | "No diagnostics found with code 0", 15 | "Found 0 codefixes", 16 | "Changes detected in the following files:" 17 | ], 18 | "changes": [ 19 | { 20 | "dataType": "Map", 21 | "value": [] 22 | } 23 | ], 24 | "filesWritten": { 25 | "dataType": "Map", 26 | "value": [] 27 | } 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteUndefinedExactOptionalPropertyTypes/a.ts: -------------------------------------------------------------------------------- 1 | import { I, J } from "./types" 2 | 3 | declare var i: I 4 | declare var j: J 5 | i = j; 6 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteUndefinedExactOptionalPropertyTypes/b.ts: -------------------------------------------------------------------------------- 1 | import { I, J } from "./types" 2 | 3 | declare var i: I 4 | declare var j: J 5 | i = j; 6 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteUndefinedExactOptionalPropertyTypes/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 2375 -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteUndefinedExactOptionalPropertyTypes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "noEmit": true 6 | } 7 | } -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteUndefinedExactOptionalPropertyTypes/types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | name: string; 3 | email?: string; 4 | } 5 | 6 | export interface I { 7 | a?: number 8 | } 9 | export interface J { 10 | a?: number | undefined 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteaddMissingOverride.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`integration tests noWriteaddMissingOverride 9 1`] = ` 4 | { 5 | "cwd": "cases/noWriteaddMissingOverride", 6 | "args": [ 7 | "-e", 8 | "4114", 9 | "-f", 10 | "fixOverrideModifier", 11 | "" 12 | ], 13 | "logs": [ 14 | "Using TypeScript 4.3.5", 15 | "Found 2 diagnostics with code 4114", 16 | "Found 2 codefixes with name fixOverrideModifier", 17 | "No codefixes found with name ", 18 | "1 changes remaining", 19 | "Overlapping changes detected. Performing additional pass...", 20 | "No diagnostics found with code 4114", 21 | "No codefixes found with name fixOverrideModifier", 22 | "No codefixes found with name ", 23 | "Changes detected in the following files:", 24 | " C:/Users/t-isabelduan/TS-transform-project/test/integration/cases/noWriteaddMissingOverride/index.ts" 25 | ], 26 | "changes": [ 27 | { 28 | "dataType": "Map", 29 | "value": [ 30 | [ 31 | "cases/noWriteaddMissingOverride/index.ts", 32 | [] 33 | ] 34 | ] 35 | }, 36 | { 37 | "dataType": "Map", 38 | "value": [] 39 | } 40 | ], 41 | "filesWritten": { 42 | "dataType": "Map", 43 | "value": [] 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteaddMissingOverride/cmd.txt: -------------------------------------------------------------------------------- 1 | ts-fix -e 4114 -f fixOverrideModifier -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteaddMissingOverride/index.ts: -------------------------------------------------------------------------------- 1 | class Base { 2 | m() {} 3 | } 4 | 5 | class Derived extends Base { 6 | m() {} 7 | } 8 | 9 | class MoreDerived extends Derived { 10 | m() {} 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/needFixing/noWriteaddMissingOverride/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitOverride": true 4 | } 5 | } -------------------------------------------------------------------------------- /test/integration/testHost.ts: -------------------------------------------------------------------------------- 1 | import { TextChange } from "typescript"; 2 | import { PathLike } from "fs"; 3 | import path from "path"; 4 | import { Host } from "../../src"; 5 | 6 | export function normalizeSlashes(path: string): string { 7 | return path.replace(/\\/g, '/'); 8 | } 9 | 10 | export function normalizeLineEndings(contents: string): string { 11 | return contents.replace(/\r\n/g, '\n'); 12 | } 13 | 14 | export function normalizeTextChange(changes: TextChange[]): TextChange[] { 15 | return changes.map((c) => { 16 | return { 17 | newText: normalizeLineEndings(c.newText), 18 | span: c.span 19 | } 20 | }) 21 | } 22 | 23 | export class TestHost implements Host { 24 | private filesWritten = new Map(); 25 | private logged: string[] = []; 26 | private existsChecked: string[] = []; 27 | private dirMade: string[] = []; 28 | private remainingChanges: (ReadonlyMap)[] = []; 29 | 30 | constructor(private cwd: string) { }; 31 | 32 | writeFile(fileName: string, content: string) { 33 | this.filesWritten.set(normalizeSlashes(path.relative(this.cwd, fileName)), content); 34 | } 35 | 36 | getRemainingChanges() { return this.remainingChanges }; 37 | 38 | addRemainingChanges(changeList: ReadonlyMap) { this.remainingChanges.push(changeList) }; 39 | 40 | 41 | log(s: string) { this.logged.push(s) }; 42 | 43 | exists(fileName: PathLike) { 44 | this.existsChecked.push(normalizeSlashes(fileName.toString())); 45 | return true; 46 | } 47 | mkdir(fileName: PathLike) { 48 | this.dirMade.push(normalizeSlashes(fileName.toString())); 49 | return undefined; 50 | } 51 | 52 | getLogs() { 53 | return this.logged; 54 | } 55 | 56 | getFilesWritten() { 57 | return this.filesWritten; 58 | } 59 | 60 | getExistsChecked() { return this.existsChecked; } 61 | 62 | getDirMade() { return this.dirMade; } 63 | 64 | getNewLine() { return "\r\n" } 65 | 66 | write(s: string) { process.stdout.write(s) }; 67 | 68 | getCanonicalFileName(fileName: string) { return fileName.toLowerCase() } 69 | 70 | getCurrentDirectory() { return process.cwd() } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": null 6 | }, 7 | "include": ["**/*"], 8 | "exclude": ["integration/cases"] 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/checkOptions.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { checkOptions } from "../../src"; 3 | import { makeOptions } from "../../src/cli"; 4 | 5 | const cwd = __dirname; 6 | 7 | test("checkOptions_emptyFilePath", async () => { 8 | const createdOption = makeOptions(cwd, []); 9 | await expect(checkOptions(createdOption)).resolves.toEqual([[], []]); 10 | }); 11 | 12 | test('checkOptions_oneInvalidFilePath', async () => { 13 | const createdOption = makeOptions(cwd, ["--file", "\\src\\index.ts"]); 14 | await expect(checkOptions(createdOption)).rejects.toThrow('All provided files are invalid'); 15 | }); 16 | 17 | test('checkOptions_oneValidFilePath', async () => { 18 | const createdOption = makeOptions(cwd, ["--file", "..\\..\\src\\index.ts"]); 19 | try { 20 | await checkOptions(createdOption); 21 | } catch (e) { 22 | await expect(checkOptions(createdOption)).rejects.toThrow(e as Error); 23 | } 24 | }); 25 | 26 | test("checkOptions_manyFilePaths", async () => { 27 | const createdOption = makeOptions(cwd, ["--file", "..\\..\\src\\index.ts", "..\\..\\src\\cli.ts", "..\\..\\src\\ts.ts"]); 28 | try { 29 | await checkOptions(createdOption); 30 | } catch (e) { 31 | await expect(checkOptions(createdOption)).rejects.toThrow(e as Error); 32 | } 33 | }); 34 | 35 | test("checkOptions_oneValidOneInvalidPath", async () => { 36 | const createdOption = makeOptions(cwd, ["--file", "..\\..\\src\\index.ts", "..\\invalid"]); 37 | try { 38 | await checkOptions(createdOption); 39 | } catch (e) { 40 | await expect(checkOptions(createdOption)).rejects.toThrow(e as Error); 41 | } 42 | }); 43 | 44 | test("checkOptions_manyValidManyInvalidPaths", async () => { 45 | const createdOption = makeOptions(cwd, ["--file", "..\\..\\src\\index.ts", "..\\..\\src\\cli.ts", "..\\..\\src\\ts.ts", "..\\invalid", "..\\invalid1", "..\\invalid2"]); 46 | try { 47 | await checkOptions(createdOption); 48 | } catch (e) { 49 | await expect(checkOptions(createdOption)).rejects.toThrow(e as Error); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/doTextChangeOnString.test.ts: -------------------------------------------------------------------------------- 1 | import { TextChange } from "typescript"; 2 | import { expect, test } from "vitest"; 3 | import { doTextChangeOnString } from "../../src/index"; 4 | 5 | test("textChangeOnString1", () => { 6 | const originalString = "012345"; 7 | const textChange: TextChange = { 8 | span: { 9 | start: 3, 10 | length: 3 11 | }, 12 | newText: "qwe" 13 | }; 14 | const expectedString = "012qwe"; 15 | expect(doTextChangeOnString(originalString, textChange)).toEqual(expectedString); 16 | }); 17 | 18 | test("textChangeOnString2", () => { 19 | const originalString = "012345"; 20 | const textChange: TextChange = { 21 | span: { 22 | start: 3, 23 | length: 0 24 | }, 25 | newText: "qwe" 26 | }; 27 | const expectedString = "012qwe345"; 28 | expect(doTextChangeOnString(originalString, textChange)).toEqual(expectedString); 29 | }); -------------------------------------------------------------------------------- /test/unit/filterCodefixesByName.test.ts: -------------------------------------------------------------------------------- 1 | import { CodeFixAction, DiagnosticCategory } from "typescript"; 2 | import { expect, test } from "vitest"; 3 | import { filterCodeFixesByFixName, FixAndDiagnostic } from "../../src/index"; 4 | 5 | const codefixes: CodeFixAction[] = [ 6 | { 7 | fixName: 'fixOverrideModifier', 8 | description: 'Add \'override\' modifier', 9 | changes: [{ fileName: 'C:/Users/t-isabelduan/Project/ts-codefix-api-example/test-project/index.ts', textChanges: [{ span: { start: 165, length: 0 }, newText: 'override ' }] }], 10 | commands: undefined, 11 | fixId: 'fixAddOverrideModifier' 12 | }, 13 | { 14 | fixName: 'fixOverrideModifier', 15 | description: 'Add \'override\' modifier', 16 | changes: [{ fileName: 'C:/Users/t-isabelduan/Project/ts-codefix-api-example/test-project/index.ts', textChanges: [{ span: { start: 244, length: 0 }, newText: 'override ' }] }], 17 | commands: undefined, 18 | fixId: 'fixAddOverrideModifier' 19 | }, 20 | { 21 | fixName: 'addConvertToUnknownForNonOverlappingTypes', 22 | description: 'Add \'unknown\' conversion for non-overlapping types', 23 | changes: [{ fileName: 'C:/Users/t-isabelduan/Project/ts-codefix-api-example/test-project/index2.ts', textChanges: [{ span: { start: 8, length: 9 }, newText: '["words"]' }] }], 24 | commands: undefined, 25 | fixId: 'addConvertToUnknownForNonOverlappingTypes' 26 | }, 27 | { 28 | fixName: 'addConvertToUnknownForNonOverlappingTypes', 29 | description: 'Add \'unknown\' conversion for non-overlapping types', 30 | changes: [{ fileName: 'C:/Users/t-isabelduan/Project/ts-codefix-api-example/test-project/index2.ts', textChanges: [{ span: { start: 30, length: 7 }, newText: '"words"' }] }], 31 | commands: undefined, 32 | fixId: 'addConvertToUnknownForNonOverlappingTypes' 33 | }, 34 | { 35 | fixName: 'addConvertToUnknownForNonOverlappingTypes', 36 | description: 'Add \'unknown\' conversion for non-overlapping types', 37 | changes: [{ fileName: 'C:/Users/t-isabelduan/Project/ts-codefix-api-example/test-project/index2.ts', textChanges: [{ span: { start: 50, length: 1 }, newText: '0' }] }], 38 | commands: undefined, 39 | fixId: 'addConvertToUnknownForNonOverlappingTypes' 40 | } 41 | ] 42 | 43 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 44 | 45 | codefixes.forEach((codefix) => { 46 | fixesAndDiagnostics.push({ 47 | fix: codefix, diagnostic: 48 | { 49 | category: DiagnosticCategory.Error, 50 | code: 322, 51 | file: undefined, 52 | start: 233, 53 | length: 10, 54 | messageText: 'test' 55 | } 56 | }); 57 | }) 58 | 59 | test("filterCodeFixesByFixName_noNamesPassedIn", () => { 60 | // empty argument behavior... currently, we just keep all fixes if none are specified 61 | const result = filterCodeFixesByFixName(fixesAndDiagnostics, []); 62 | expect(result[0]).toEqual(fixesAndDiagnostics); 63 | expect(result[1]).toEqual(["Found 5 codefixes"]); 64 | }) 65 | 66 | test("filterCodeFixesByFixName_allNamesPassedIn", () => { 67 | const result = filterCodeFixesByFixName(fixesAndDiagnostics, ['fixOverrideModifier', 'addConvertToUnknownForNonOverlappingTypes']); 68 | expect(result[0]).toEqual(fixesAndDiagnostics); 69 | expect(result[1]).toEqual([ 70 | "Found 2 codefixes with name fixOverrideModifier", 71 | "Found 3 codefixes with name addConvertToUnknownForNonOverlappingTypes" 72 | ]) 73 | }) 74 | 75 | test("filterCodeFixesByFixName_singleStringPassedIn", () => { 76 | const result = filterCodeFixesByFixName(fixesAndDiagnostics, ['fixOverrideModifier']); 77 | expect(result[0]).toEqual(fixesAndDiagnostics.slice(0, 2)); 78 | expect(result[1]).toEqual([ 79 | "Found 2 codefixes with name fixOverrideModifier"]) 80 | }) 81 | 82 | test("filterCodeFixesByFixName_singleStringListPassedIn", () => { 83 | const result = filterCodeFixesByFixName(fixesAndDiagnostics, ['addConvertToUnknownForNonOverlappingTypes']); 84 | expect(result[0]).toEqual(fixesAndDiagnostics.slice(2, 5)); 85 | expect(result[1]).toEqual([ 86 | "Found 3 codefixes with name addConvertToUnknownForNonOverlappingTypes" 87 | ]) 88 | }) 89 | 90 | test("filterCodeFixesByFixName_noMatch", () => { 91 | const result = filterCodeFixesByFixName(fixesAndDiagnostics, ['add']); 92 | expect(result[0]).toEqual([]); 93 | expect(result[1]).toEqual(["No codefixes found with name add"]); 94 | }) 95 | 96 | test("filterCodeFixesByFixName_noAndSomeMatch", () => { 97 | const result = filterCodeFixesByFixName(fixesAndDiagnostics, ['fixOverrideModifier', 'add']); 98 | expect(result[0]).toEqual(fixesAndDiagnostics.slice(0, 2)); 99 | expect(result[1]).toEqual([ 100 | "Found 2 codefixes with name fixOverrideModifier", 101 | "No codefixes found with name add"]); 102 | }) 103 | -------------------------------------------------------------------------------- /test/unit/filterDiagnosticsByFileAndErrorCode.test.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, DiagnosticCategory, SourceFile } from "typescript"; 2 | import { expect, test } from "vitest"; 3 | import { filterDiagnosticsByFileAndErrorCode } from "../../src"; 4 | 5 | const default_codes: number[] = []; 6 | 7 | function makeDiagnostic(code: number, fileName?: string, file?: SourceFile): Diagnostic { 8 | return { 9 | category: 1, 10 | code: code, 11 | file: file ? { 12 | ...file, 13 | fileName: fileName ? fileName : "", 14 | } : undefined, 15 | start: undefined, 16 | length: undefined, 17 | messageText: { 18 | messageText: "", 19 | category: DiagnosticCategory.Error, 20 | code: 233 21 | } 22 | 23 | } 24 | } 25 | 26 | test("filterDiagnosticsByFileAndErrorCode_noErrorsInOpt_noPaths", () => { 27 | const originalDiagnostics = [[makeDiagnostic(111), makeDiagnostic(222), makeDiagnostic(333)]]; 28 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, default_codes, []); 29 | expect(results[0]).toEqual(originalDiagnostics); 30 | expect(results[1]).toEqual(["Found 3 diagnostics in 1 files"]); 31 | }) 32 | 33 | // test("filterDiagnosticsByFileAndErrorCode_noErrorsInOpt_OnePathNoMathchingFiles", () => { 34 | // const originalDiagnostics = [[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)]]; 35 | // const validFiles = ["b.ts"]; 36 | // const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, default_codes, validFiles); 37 | // expect(results[0]).toEqual([]); 38 | // expect(results[1]).toEqual(["No diagnostics found for files"]); 39 | // }) 40 | 41 | test("filterDiagnosticsByFileAndErrorCode_noErrorsInOpt_oneFile", () => { 42 | const originalDiagnostics = [[makeDiagnostic(111), makeDiagnostic(222), makeDiagnostic(333)]]; 43 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, default_codes); 44 | expect(results[0]).toEqual(originalDiagnostics); 45 | expect(results[1]).toEqual(["Found 3 diagnostics in 1 files"]); 46 | 47 | const diagnosticsRepeatedError = [[makeDiagnostic(111), makeDiagnostic(333), makeDiagnostic(111)]]; 48 | const resultsRepeatedError = filterDiagnosticsByFileAndErrorCode(diagnosticsRepeatedError, default_codes); 49 | expect(resultsRepeatedError[0]).toEqual(diagnosticsRepeatedError); 50 | expect(resultsRepeatedError[1]).toEqual(["Found 3 diagnostics in 1 files"]); 51 | }) 52 | 53 | // test("filterDiagnosticsByFileAndErrorCode_noErrorsInOpt_oneValidFilePath", () => { 54 | // const originalDiagnostics = [[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)]]; 55 | // const validFiles = ["a.ts"]; 56 | // const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, default_codes, validFiles); 57 | // expect(results[0]).toEqual(originalDiagnostics); 58 | // expect(results[1]).toEqual(["Found 3 diagnostics for the given files"]); 59 | // }) 60 | 61 | test("filterDiagnosticsByFileAndErrorCode_noErrorsInOpt_multiFiles", () => { 62 | const originalDiagnostics = [[makeDiagnostic(111), makeDiagnostic(222), makeDiagnostic(333)], 63 | [makeDiagnostic(444), makeDiagnostic(444), makeDiagnostic(111)]]; 64 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, default_codes); 65 | expect(results[0]).toEqual(originalDiagnostics); 66 | expect(results[1]).toEqual(["Found 6 diagnostics in 2 files"]); 67 | 68 | const diagnosticsRepeatedFile = [[makeDiagnostic(111), makeDiagnostic(222), makeDiagnostic(333)], 69 | [makeDiagnostic(111), makeDiagnostic(222), makeDiagnostic(333)], 70 | [makeDiagnostic(444), makeDiagnostic(444)]]; 71 | const resultsRepeatedFile = filterDiagnosticsByFileAndErrorCode(diagnosticsRepeatedFile, default_codes); 72 | expect(resultsRepeatedFile[0]).toEqual(diagnosticsRepeatedFile); 73 | expect(resultsRepeatedFile[1]).toEqual(["Found 8 diagnostics in 3 files"]); 74 | }) 75 | 76 | // test("filterDiagnosticsByFileAndErrorCode_noErrorsInOpt_oneValidFilePathForMultiFiles", () => { 77 | // const originalDiagnostics = [[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)], 78 | // [makeDiagnostic(444, "b.ts", sourceFile), makeDiagnostic(444, "b.ts", sourceFile), makeDiagnostic(111, "b.ts", sourceFile)]]; 79 | // const validFiles = ["a.ts"]; 80 | // const updatedDiagnostics = [[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)]]; 81 | // const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, default_codes, validFiles); 82 | // expect(results[0]).toEqual(updatedDiagnostics); 83 | // expect(results[1]).toEqual(["Found 3 diagnostics for the given files"]); 84 | // }) 85 | 86 | test("filterDiagnosticsByFileAndErrorCode_oneErrorInOpt_oneFile", () => { 87 | const originalDiagnostics = [[makeDiagnostic(111), makeDiagnostic(222), makeDiagnostic(333)]]; 88 | const results111 = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111]); 89 | expect(results111[0]).toEqual([[makeDiagnostic(111)]]); 90 | expect(results111[1]).toEqual(["Found 1 diagnostics with code 111"]); 91 | 92 | const results333 = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [333]); 93 | expect(results333[0]).toEqual([[makeDiagnostic(333)]]); 94 | expect(results333[1]).toEqual(["Found 1 diagnostics with code 333"]); 95 | 96 | const diagnosticsRepeatedError = [[makeDiagnostic(111), makeDiagnostic(333), makeDiagnostic(111)]]; 97 | const resultsRepeatedError = filterDiagnosticsByFileAndErrorCode(diagnosticsRepeatedError, [111]); 98 | expect(resultsRepeatedError[0]).toEqual([[makeDiagnostic(111), makeDiagnostic(111)]]); 99 | expect(resultsRepeatedError[1]).toEqual(["Found 2 diagnostics with code 111"]); 100 | 101 | const resultsRepeatedError333 = filterDiagnosticsByFileAndErrorCode(diagnosticsRepeatedError, [333]); 102 | expect(resultsRepeatedError333[0]).toEqual([[makeDiagnostic(333)]]); 103 | expect(resultsRepeatedError333[1]).toEqual(["Found 1 diagnostics with code 333"]); 104 | }) 105 | 106 | // test("filterDiagnosticsByFileAndErrorCode_oneErrorInOpt_oneFilePathAndOneFile", () => { 107 | // const originalDiagnostics = [[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)]]; 108 | // const validFiles111 = ["a.ts"]; 109 | // const results111 = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111], validFiles111); 110 | // expect(results111[0]).toEqual([[makeDiagnostic(111, "a.ts", sourceFile)]]); 111 | // expect(results111[1]).toEqual(["Found 3 diagnostics for the given files", "Found 1 diagnostics with code 111"]); 112 | 113 | // const validFiles333 = ["a.ts"]; 114 | // const results333 = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [333], validFiles333); 115 | // expect(results333[0]).toEqual([[makeDiagnostic(333, "a.ts", sourceFile)]]); 116 | // expect(results333[1]).toEqual(["Found 3 diagnostics for the given files", "Found 1 diagnostics with code 333"]); 117 | 118 | // const validFilesRepeatedError = ["a.ts"]; 119 | // const diagnosticsRepeatedError = [[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile), makeDiagnostic(111, "a.ts", sourceFile)]]; 120 | // const resultsRepeatedError = filterDiagnosticsByFileAndErrorCode(diagnosticsRepeatedError, [111], validFilesRepeatedError); 121 | // expect(resultsRepeatedError[0]).toEqual([[makeDiagnostic(111, "a.ts", sourceFile), makeDiagnostic(111, "a.ts", sourceFile)]]); 122 | // expect(resultsRepeatedError[1]).toEqual(["Found 3 diagnostics for the given files", "Found 2 diagnostics with code 111"]); 123 | 124 | // const validFiles333RepeatedError = ["a.ts"]; 125 | // const resultsRepeatedError333 = filterDiagnosticsByFileAndErrorCode(diagnosticsRepeatedError, [333], validFiles333RepeatedError); 126 | // expect(resultsRepeatedError333[0]).toEqual([[makeDiagnostic(333, "a.ts", sourceFile)]]); 127 | // expect(resultsRepeatedError333[1]).toEqual(["Found 3 diagnostics for the given files", "Found 1 diagnostics with code 333"]); 128 | // }); 129 | 130 | test("filterDiagnosticsByFileAndErrorCode_oneErrorInOptNotFoundInOneFile", () => { 131 | const originalDiagnostics = [[makeDiagnostic(222), makeDiagnostic(222), makeDiagnostic(333)]]; 132 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111]); 133 | expect(results[0]).toEqual([]); 134 | expect(results[1]).toEqual(["No diagnostics found with code 111"]); 135 | }); 136 | 137 | test("filterDiagnosticsByFileAndErrorCode_manyErrorInOptNotFoundInOneFile", () => { 138 | const originalDiagnostics = [[makeDiagnostic(222), makeDiagnostic(222), makeDiagnostic(333)]]; 139 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111, 999]); 140 | expect(results[0]).toEqual([]); 141 | expect(results[1]).toEqual(["No diagnostics found with code 111", "No diagnostics found with code 999"]); 142 | }); 143 | 144 | // test("filterDiagnosticsByFileAndErrorCode_manyErrorInOptNotFoundInOneFileAndOneValidFilePath", () => { 145 | // const originalDiagnostics = [[makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)]]; 146 | // const validFiles = ["a.ts"] 147 | // const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111, 999], validFiles); 148 | // expect(results[0]).toEqual([]); 149 | // expect(results[1]).toEqual(["Found 3 diagnostics for the given files", "No diagnostics found with code 111", "No diagnostics found with code 999"]); 150 | // }) 151 | 152 | test("filterDiagnosticsByFileAndErrorCode_oneErrorInOptNotFoundInManyFiles", () => { 153 | const originalDiagnostics = [[makeDiagnostic(222), makeDiagnostic(222), makeDiagnostic(333)], 154 | [makeDiagnostic(444), makeDiagnostic(444), makeDiagnostic(777)], 155 | [makeDiagnostic(777), makeDiagnostic(222), makeDiagnostic(777)], 156 | [makeDiagnostic(333), makeDiagnostic(222)]]; 157 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111]); 158 | expect(results[0]).toEqual([]); 159 | expect(results[1]).toEqual(["No diagnostics found with code 111"]); 160 | }) 161 | 162 | // test("filterDiagnosticsByFileAndErrorCode_oneErrorInOptNotFoundInManyFilesAndTwoValidFilePaths", () => { 163 | // const originalDiagnostics = [[makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(222, "a.ts", sourceFile), makeDiagnostic(333, "a.ts", sourceFile)], 164 | // [makeDiagnostic(444), makeDiagnostic(444), makeDiagnostic(777)], 165 | // [makeDiagnostic(777, "b.ts", sourceFile), makeDiagnostic(222, "b.ts", sourceFile), makeDiagnostic(777, "b.ts", sourceFile)], 166 | // [makeDiagnostic(333), makeDiagnostic(222)]]; 167 | // const validFiles = ["a.ts", "b.ts"]; 168 | // const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111], validFiles); 169 | // expect(results[0]).toEqual([]); 170 | // expect(results[1]).toEqual(["Found 6 diagnostics for the given files", "No diagnostics found with code 111"]); 171 | // }); 172 | 173 | test("filterDiagnosticsByFileAndErrorCode_oneErrorInOptSomeFoundInManyFiles", () => { 174 | const originalDiagnostics = [[makeDiagnostic(222, "f1"), makeDiagnostic(111, "f1"), makeDiagnostic(333, "f1")], 175 | [makeDiagnostic(444, "f2"), makeDiagnostic(444, "f2"), makeDiagnostic(777, "f2")], 176 | [makeDiagnostic(777, "f3"), makeDiagnostic(222, "f3"), makeDiagnostic(777, "f3")], 177 | [makeDiagnostic(333, "f4"), makeDiagnostic(222, "f4")], 178 | [makeDiagnostic(111, "f5"), makeDiagnostic(222, "f5"), makeDiagnostic(333, "f5")], 179 | [makeDiagnostic(222, "f6"), makeDiagnostic(111, "f6"), makeDiagnostic(111, "f6")], 180 | [makeDiagnostic(444, "f7"), makeDiagnostic(444, "f7")], 181 | [makeDiagnostic(111, "f8")]]; 182 | const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111]); 183 | expect(results[0]).toEqual([[makeDiagnostic(111, "f1")], 184 | [makeDiagnostic(111, "f5")], 185 | [makeDiagnostic(111, "f6"), makeDiagnostic(111, "f6")], 186 | [makeDiagnostic(111, "f8")]]); 187 | expect(results[1]).toEqual(["Found 5 diagnostics with code 111"]); 188 | }); 189 | 190 | // test("filterDiagnosticsByFileAndErrorCode_oneErrorInOptSomeFoundInManyFilesAndManyFilePaths", () => { 191 | // const originalDiagnostics = [[makeDiagnostic(222, "f1", sourceFile), makeDiagnostic(111, "f1", sourceFile), makeDiagnostic(333, "f1", sourceFile)], 192 | // [makeDiagnostic(444, "f2", sourceFile), makeDiagnostic(444, "f2", sourceFile), makeDiagnostic(777, "f2", sourceFile)], 193 | // [makeDiagnostic(777, "f3", sourceFile), makeDiagnostic(222, "f3", sourceFile), makeDiagnostic(777, "f3", sourceFile)], 194 | // [makeDiagnostic(333, "f4", sourceFile), makeDiagnostic(222, "f4", sourceFile)], 195 | // [makeDiagnostic(111, "f5", sourceFile), makeDiagnostic(222, "f5", sourceFile), makeDiagnostic(333, "f5", sourceFile)], 196 | // [makeDiagnostic(222, "f6", sourceFile), makeDiagnostic(111, "f6", sourceFile), makeDiagnostic(111, "f6", sourceFile)], 197 | // [makeDiagnostic(444, "f7", sourceFile), makeDiagnostic(444, "f7", sourceFile)], 198 | // [makeDiagnostic(111, "f8", sourceFile)]]; 199 | // const validFiles = ["f2", "f8", "f7", "f6"]; 200 | // const results = filterDiagnosticsByFileAndErrorCode(originalDiagnostics, [111], validFiles); 201 | // expect(results[0]).toEqual([[makeDiagnostic(111, "f6", sourceFile), makeDiagnostic(111, "f6", sourceFile)], 202 | // [makeDiagnostic(111, "f8", sourceFile)]]); 203 | // expect(results[1]).toEqual(["Found 9 diagnostics for the given files", "Found 3 diagnostics with code 111"]); 204 | // }) 205 | -------------------------------------------------------------------------------- /test/unit/getAllNoAppliedChangesByFile.test.ts: -------------------------------------------------------------------------------- 1 | import { CodeFixAction } from 'typescript'; 2 | import { expect, test } from "vitest"; 3 | import { getAllNoAppliedChangesByFile } from './../../src'; 4 | 5 | const allNoAppliedChanges = new Map>; 6 | const noAppliedChanges: CodeFixAction[] = [ 7 | { fixName: 'fixMissingConstraint', description: '', changes: [{ fileName: 'noAppliedChangesFile', textChanges: [] }] }, 8 | { fixName: 'fixMissingConstraint', description: '', changes: [{ fileName: 'noAppliedChangesFile', textChanges: [] }] }, 9 | { fixName: 'import', description: '', changes: [{ fileName: 'noAppliedChangesFile', textChanges: [] }], commands: ['AddMissingImports'] } 10 | ]; 11 | 12 | const allNoAppliedChanges1 = new Map>; 13 | const noAppliedChangesSet1 = new Set; 14 | noAppliedChangesSet1.add('testfile'); 15 | noAppliedChangesSet1.add('testfile1'); 16 | const noAppliedChangesSet2 = new Set; 17 | noAppliedChangesSet2.add('testfile2'); 18 | noAppliedChangesSet2.add('testfile3'); 19 | allNoAppliedChanges1.set('fixOverride', noAppliedChangesSet1); 20 | allNoAppliedChanges1.set('fixMissingMember', noAppliedChangesSet2); 21 | 22 | test("getAllNoAppliedChangesByFile_startWithEmptyMap", () => { 23 | const result = getAllNoAppliedChangesByFile(allNoAppliedChanges, noAppliedChanges); 24 | const resultSet = new Set; 25 | resultSet.add('noAppliedChangesFile'); 26 | const resultMap = new Map>; 27 | resultMap.set('fixMissingConstraint', resultSet); 28 | resultMap.set('import', resultSet); 29 | expect(result).toEqual(resultMap); 30 | }); 31 | 32 | test("getAllNoAppliedChangesByFile_startWithNonEmptyMap", () => { 33 | const result = getAllNoAppliedChangesByFile(allNoAppliedChanges1, noAppliedChanges); 34 | const resultSet = new Set; 35 | resultSet.add('noAppliedChangesFile'); 36 | const resultSet1 = new Set; 37 | resultSet1.add('testfile'); 38 | resultSet1.add('testfile1'); 39 | const resultSet2 = new Set; 40 | resultSet2.add('testfile2'); 41 | resultSet2.add('testfile3'); 42 | const resultSet3 = new Set; 43 | resultSet3.add('noAppliedChangesFile'); 44 | const resultMap = new Map>; 45 | resultMap.set('fixMissingConstraint', resultSet); 46 | resultMap.set('fixOverride', resultSet1); 47 | resultMap.set('fixMissingMember', resultSet2); 48 | resultMap.set('import', resultSet3); 49 | expect(result).toEqual(resultMap); 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/getTextChangeDict.test.ts: -------------------------------------------------------------------------------- 1 | import { CodeFixAction } from "typescript"; 2 | import { expect, test } from "vitest"; 3 | import { getTextChangeDict } from "../../src/index"; 4 | 5 | const codefixes: CodeFixAction[] = [ 6 | { 7 | fixName: 'fixOverrideModifier', 8 | description: 'Add \'override\' modifier', 9 | changes: [{ fileName: 'foo.ts', textChanges: [{ span: { start: 2, length: 0 }, newText: 'override ' }, { span: { start: 3, length: 0 }, newText: 'override ' }] }], 10 | commands: undefined, 11 | fixId: 'fixAddOverrideModifier' 12 | }, 13 | { 14 | fixName: 'fixOverrideModifier', 15 | description: 'Add \'override\' modifier', 16 | changes: [{ fileName: 'foo.ts', textChanges: [{ span: { start: 1, length: 0 }, newText: 'override ' }] }], 17 | commands: undefined, 18 | fixId: 'fixAddOverrideModifier' 19 | }, 20 | { 21 | fixName: 'addConvertToUnknownForNonOverlappingTypes', 22 | description: 'Add \'unknown\' conversion for non-overlapping types', 23 | changes: [{ fileName: 'foo.ts', textChanges: [{ span: { start: 8, length: 9 }, newText: '["words"]' }] }], 24 | commands: undefined, 25 | fixId: 'addConvertToUnknownForNonOverlappingTypes' 26 | }, 27 | ] 28 | 29 | test("should merge text changes in order", () => { 30 | const result = getTextChangeDict(codefixes); 31 | const changes = result.get('foo.ts'); 32 | const spanStarts = changes?.map(c => c.span.start); 33 | expect(spanStarts).toEqual([1, 2, 3, 8]); 34 | }) 35 | 36 | -------------------------------------------------------------------------------- /test/unit/makeOptions.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { expect, test } from "vitest"; 3 | import { makeOptions } from "../../src/cli"; 4 | 5 | // TODO: uhh the defult cwd may not nessssrily resolve to correct path on non windows 6 | // TODO: the way to resolve above is never use absolute paths in any test. :( 7 | test("makeOptions_empty_argv", () => { 8 | const cwd = __dirname; 9 | const createdOption = makeOptions(cwd, []); 10 | expect(createdOption.tsconfig).toEqual(path.resolve(cwd, "tsconfig.json")); 11 | expect(createdOption.outputFolder).toEqual(path.resolve(cwd)); 12 | expect(createdOption.errorCode).toEqual([]); 13 | expect(createdOption.fixName).toEqual([]); 14 | }); 15 | 16 | test("makeOptions_withOutputFolder", () => { 17 | const cwd = __dirname; 18 | const createdOption_partial = makeOptions(cwd, ["-o", "..\\DifferentOutput2"]); 19 | expect(createdOption_partial.outputFolder).toEqual(path.resolve(path.dirname(createdOption_partial.tsconfig), "..\\DifferentOutput2")); 20 | }); 21 | 22 | test("makeOptions_errorCode_singleError", () => { 23 | const cwd = __dirname; 24 | const createdOption = makeOptions(cwd, ["-e", "1"]); 25 | expect(createdOption.errorCode).toEqual([1]); 26 | }) 27 | 28 | test("makeOptions_errorCode_manyError", () => { 29 | const cwd = __dirname; 30 | const createdOption = makeOptions(cwd, ["-e", "0", "123", "41", "1"]); 31 | expect(createdOption.errorCode).toEqual([0, 123, 41, 1]); 32 | }) 33 | 34 | test("makeOptions_fixName_singlefixName", () => { 35 | const cwd = __dirname; 36 | const createdOption = makeOptions(cwd, ["-f", "fixOverride"]); 37 | expect(createdOption.fixName).toEqual(["fixOverride"]); 38 | }) 39 | 40 | test("makeOptions_fixName_manyfixName", () => { 41 | // these names aren't necessarily real errors 42 | const cwd = __dirname; 43 | const createdOption = makeOptions(cwd, ["-f", "fixOverride", "fixUnknown", "fixUndefined"]); 44 | expect(createdOption.fixName).toEqual(["fixOverride", "fixUnknown", "fixUndefined"]); 45 | }) 46 | 47 | test("makeOptions_singleFilePaths", () => { 48 | const cwd = __dirname; 49 | const createdOption = makeOptions(cwd, ["--file", "..\\src\\index.ts"]); 50 | expect(createdOption.file).toEqual(["..\\src\\index.ts"]); 51 | }); 52 | 53 | 54 | test("makeOptions_manyFilePaths", () => { 55 | const cwd = __dirname; 56 | const createdOption = makeOptions(cwd, ["--file", "..\\src\\index.ts", "..\\src\\cli.ts", "..\\src\\ts.ts"]); 57 | expect(createdOption.file).toEqual(["..\\src\\index.ts", "..\\src\\cli.ts", "..\\src\\ts.ts"]); 58 | }); 59 | 60 | test("makeOptions_showMultiple", () => { 61 | const cwd = __dirname; 62 | const createdOption = makeOptions(cwd, ["--showMultiple"]); 63 | expect(createdOption.showMultiple).toEqual(true); 64 | }); 65 | 66 | test("makeOptions_MutlipleOptions", () => { 67 | const cwd = __dirname; 68 | const createdOption = makeOptions(cwd, ["-f", "fixOverride", "--file", "..\\src\\index.ts", "--showMultiple", "--ignoreGitStatus"]); 69 | expect(createdOption.fixName).toEqual(["fixOverride"]); 70 | expect(createdOption.file).toEqual(["..\\src\\index.ts"]); 71 | expect(createdOption.showMultiple).toEqual(true); 72 | expect(createdOption.write).toEqual(false); 73 | expect(createdOption.ignoreGitStatus).toEqual(true); 74 | }); 75 | 76 | test("makeOptions_MutlipleOptions1", () => { 77 | const cwd = __dirname; 78 | const createdOption = makeOptions(cwd, ["-f", "fixOverride", "--file", "..\\src\\index.ts", "..\\src\\cli.ts", "..\\src\\ts.ts", "--showMultiple", "-w", "-o", "..\\DifferentOutput2"]); 79 | expect(createdOption.fixName).toEqual(["fixOverride"]); 80 | expect(createdOption.file).toEqual(["..\\src\\index.ts", "..\\src\\cli.ts", "..\\src\\ts.ts"]); 81 | expect(createdOption.showMultiple).toEqual(true); 82 | expect(createdOption.write).toEqual(true); 83 | expect(createdOption.outputFolder).toEqual(path.resolve(path.dirname(createdOption.tsconfig), "..\\DifferentOutput2")); 84 | expect(createdOption.errorCode).toEqual([]); 85 | expect(createdOption.ignoreGitStatus).toEqual(false); 86 | }); 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /test/unit/outputFileName.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { expect, test } from "vitest"; 3 | import { makeOptions } from "../../src/cli"; 4 | import { getOutputFilePath } from "../../src/index"; 5 | 6 | test("outputFileName_replace", () => { 7 | const dir = __dirname; 8 | const default_opt = makeOptions(dir, []); 9 | const files = [path.resolve(dir, "file1.ts"), path.resolve(dir, "src/folder/file2.ts")]; 10 | 11 | expect(getOutputFilePath(files[0], default_opt)).toEqual(files[0]); 12 | expect(getOutputFilePath(files[1], default_opt)).toEqual(files[1]); 13 | }) 14 | 15 | test("outputFileName_output_noneGiven", () => { 16 | const dir = __dirname; 17 | const default_opt = makeOptions(dir, ["-o", "../Output"]); 18 | const files = [path.resolve(dir, "file1.ts"), path.resolve(dir, "src/folder/file2.ts")]; 19 | 20 | expect(getOutputFilePath(files[0], default_opt)).toEqual(path.resolve(dir, "../Output", "file1.ts")); 21 | expect(getOutputFilePath(files[1], default_opt)).toEqual(path.resolve(dir, "../Output", "src/folder/file2.ts")); 22 | }) -------------------------------------------------------------------------------- /test/unit/pathOperations.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { expect, test } from "vitest"; 3 | import { getDirectory, getFileName } from "../../src/index"; 4 | 5 | const cwd = process.cwd(); 6 | 7 | const fileList = ["file1.ts", (path.normalize("/src/file2.ts"))]; 8 | 9 | 10 | test("getFileName", () => { 11 | expect(getFileName(fileList[0])).toEqual("file1.ts"); 12 | expect(getFileName(fileList[1])).toEqual("file2.ts"); 13 | expect(getFileName(path.resolve(cwd, fileList[0]))).toEqual("file1.ts"); 14 | expect(getFileName(path.resolve(cwd, fileList[1]))).toEqual("file2.ts"); 15 | }) 16 | 17 | test("getDirectory", () => { 18 | expect(path.normalize(getDirectory(fileList[0]))).toEqual(path.normalize(".")); 19 | expect(path.normalize(getDirectory(fileList[1]))).toEqual(path.normalize("/src")); 20 | expect(path.normalize(getDirectory(path.resolve(cwd, fileList[0])))).toEqual(path.normalize(cwd)); 21 | expect(path.normalize(getDirectory(path.resolve(cwd, fileList[1])))).toEqual(path.resolve(cwd, "/src")); 22 | }) 23 | 24 | 25 | // test("getRelativePath", () => { 26 | // expect(getRelativePath(fileList[0], opt_default)).toEqual("file1.ts"); 27 | // 28 | // test issue: 29 | // expect(getRelativePath(fileList[1], opt_default)).toEqual(path.normalize("src/file2.ts")); 30 | // Expected: "src\\file2.ts" 31 | // Received: "..\\..\\..\\src\\file2.ts" 32 | // }) 33 | 34 | // test("getOutputFilePath", () => { 35 | 36 | // }) 37 | -------------------------------------------------------------------------------- /test/unit/removeDuplicatedFixes.test.ts: -------------------------------------------------------------------------------- 1 | import { CodeFixAction, DiagnosticCategory } from "typescript"; 2 | import { expect, test } from "vitest"; 3 | import { FixAndDiagnostic, removeDuplicatedFixes } from "../../src"; 4 | 5 | //This test is probably not be too effective since splice is being used 6 | function makeCodeFix(fixName: string, start: number, length: number): CodeFixAction { 7 | return { 8 | fixName: fixName, 9 | description: 'fix description goes here', 10 | changes: [{ fileName: '', textChanges: [{ span: { start: start, length: length }, newText: 'override ' }] }], 11 | commands: undefined, 12 | fixId: fixName 13 | } 14 | } 15 | 16 | function makeFixAndDiagnostic(codefix: CodeFixAction, messageText: string): FixAndDiagnostic { 17 | return { 18 | fix: codefix, 19 | diagnostic: { 20 | category: 1, 21 | code: 324, 22 | file: undefined, 23 | start: undefined, 24 | length: undefined, 25 | messageText: { 26 | messageText: messageText, 27 | category: DiagnosticCategory.Error, 28 | code: 233 29 | }, 30 | } 31 | } 32 | } 33 | 34 | test("removeDuplicatedFixes_twoEqualFixes", () => { 35 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 36 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0)]; 37 | codefixes.forEach((codefix) => { 38 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 'codefix')); 39 | }) 40 | const result = removeDuplicatedFixes(fixesAndDiagnostics); 41 | expect(result).toEqual(fixesAndDiagnostics); 42 | }); 43 | 44 | test("removeDuplicatedFixes_threeEqualFixes", () => { 45 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 46 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0)]; 47 | codefixes.forEach((codefix) => { 48 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 'codefix')); 49 | }) 50 | const result = removeDuplicatedFixes(fixesAndDiagnostics); 51 | expect(result).toEqual(fixesAndDiagnostics); 52 | }); 53 | 54 | test("removeDuplicatedFixes_manyEqualFixes", () => { 55 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 56 | let i = 0; 57 | while (i < 10) { 58 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 'codefix')); 59 | i++; 60 | } 61 | const result = removeDuplicatedFixes(fixesAndDiagnostics); 62 | expect(result).toEqual(fixesAndDiagnostics); 63 | }); 64 | 65 | test("removeDuplicatedFixes_allDifferentFixes", () => { 66 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 67 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), makeCodeFix('fixOverrideModifier', 165, 0), 68 | makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 9, 64), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 40, 73)]; 69 | codefixes.forEach((codefix) => { 70 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 'codefix')); 71 | }) 72 | const result = removeDuplicatedFixes(fixesAndDiagnostics); 73 | expect(result).toEqual(fixesAndDiagnostics); 74 | }); 75 | 76 | test("removeDuplicatedFixes_twoEqualFixesAndOneDifferent", () => { 77 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 78 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), makeCodeFix('fixOverrideModifier', 165, 0)]; 79 | codefixes.forEach((codefix) => { 80 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 'codefix')); 81 | }) 82 | const result = removeDuplicatedFixes(fixesAndDiagnostics); 83 | expect(result).toEqual(fixesAndDiagnostics); 84 | }); 85 | 86 | test("removeDuplicatedFixes_someDifferentAndSomeDuplicatedFixes", () => { 87 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 88 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 9, 64), 89 | makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0), 90 | makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), 91 | makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 40, 73), 92 | makeCodeFix('fixOverrideModifier', 165, 0)]; 93 | codefixes.forEach((codefix) => { 94 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 'codefix')); 95 | }) 96 | const result = removeDuplicatedFixes(fixesAndDiagnostics); 97 | expect(result).toEqual(fixesAndDiagnostics); 98 | }); -------------------------------------------------------------------------------- /test/unit/removeMultipleDiagnostics.test.ts: -------------------------------------------------------------------------------- 1 | import { CodeFixAction, DiagnosticCategory } from "typescript"; 2 | import { expect, test } from "vitest"; 3 | import { Choices, FixAndDiagnostic, removeMultipleDiagnostics } from "../../src"; 4 | 5 | //This test is probably not be too effective since splice is being used 6 | function makeCodeFix(fixName: string, start: number, length: number): CodeFixAction { 7 | return { 8 | fixName: fixName, 9 | description: 'fix description goes here', 10 | changes: [{ fileName: '', textChanges: [{ span: { start: start, length: length }, newText: 'override ' }] }], 11 | commands: undefined, 12 | fixId: fixName 13 | } 14 | } 15 | 16 | function makeFixAndDiagnostic(codefix: CodeFixAction, code: number, lenght: number, start: number): FixAndDiagnostic { 17 | return { 18 | fix: codefix, 19 | diagnostic: { 20 | category: 1, 21 | code: code, 22 | file: undefined, 23 | start: start, 24 | length: lenght, 25 | messageText: { 26 | messageText: '', 27 | category: DiagnosticCategory.Error, 28 | code: 233 29 | }, 30 | } 31 | } 32 | } 33 | 34 | test("removeDuplicatedFixes_twoEqualDiagnostics", () => { 35 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 36 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0)]; 37 | codefixes.forEach((codefix) => { 38 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 435, 4, 5)); 39 | }) 40 | const result = removeMultipleDiagnostics(fixesAndDiagnostics); 41 | expect(result).toEqual([fixesAndDiagnostics[0]]); 42 | }); 43 | 44 | test("removeDuplicatedFixes_threeEqualDiagnostics", () => { 45 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 46 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0)]; 47 | codefixes.forEach((codefix) => { 48 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefix, 435, 4, 5)); 49 | }) 50 | const result = removeMultipleDiagnostics(fixesAndDiagnostics); 51 | expect(result).toEqual([fixesAndDiagnostics[0]]); 52 | }); 53 | 54 | test("removeDuplicatedFixes_manyEqualDiagnostics", () => { 55 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 56 | let i = 0; 57 | while (i < 10) { 58 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 59 | i++; 60 | } 61 | const result = removeMultipleDiagnostics(fixesAndDiagnostics); 62 | expect(result).toEqual([fixesAndDiagnostics[0]]); 63 | }); 64 | 65 | test("removeDuplicatedFixes_allDifferentDiagnostics", () => { 66 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 67 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), makeCodeFix('fixOverrideModifier', 165, 0), 68 | makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 9, 64), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 40, 73)]; 69 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[0], 435, 4, 5)); 70 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[1], 455, 4, 5)); 71 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[2], 4333, 5, 64)); 72 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[3], 764, 7, 64)); 73 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[4], 422, 2, 544)); 74 | const result = removeMultipleDiagnostics(fixesAndDiagnostics); 75 | expect(result).toEqual(fixesAndDiagnostics); 76 | }); 77 | 78 | test("removeDuplicatedFixes_twoEqualDiagnosticsAndOneDifferent", () => { 79 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 80 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 81 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), 543, 4, 5)); 82 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 83 | const result = removeMultipleDiagnostics(fixesAndDiagnostics); 84 | expect(result).toEqual(fixesAndDiagnostics); 85 | }); 86 | 87 | test("removeDuplicatedFixes_someDifferentAndSomeDuplicatedDiagnostics", () => { 88 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 89 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 90 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 9, 64), 435, 4, 5)); 91 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 543, 4, 5)); 92 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 93 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 543, 4, 5)); 94 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), 765, 4, 5)); 95 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 96 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 97 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 40, 73), 765, 44, 5)); 98 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 99 | const result = removeMultipleDiagnostics(fixesAndDiagnostics); 100 | expect(result).toEqual(fixesAndDiagnostics); 101 | }); 102 | 103 | test("removeDuplicatedFixes_acceptOneOutofOne", () => { 104 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 105 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 106 | const result = removeMultipleDiagnostics(fixesAndDiagnostics, Choices.ACCEPT); 107 | expect(result).toEqual(fixesAndDiagnostics); 108 | }); 109 | 110 | 111 | test("removeDuplicatedFixes_acceptAllTheSameCode", () => { 112 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 113 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 114 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 9, 64), 435, 4, 5)); 115 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 116 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 6, 5)); 117 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 118 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 8, 9), 435, 9, 5)); 119 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 120 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 121 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 40, 73), 435, 44, 5)); 122 | fixesAndDiagnostics.push(makeFixAndDiagnostic(makeCodeFix('fixOverrideModifier', 165, 0), 435, 4, 5)); 123 | const result = removeMultipleDiagnostics(fixesAndDiagnostics, Choices.ACCEPT); 124 | expect(result).toEqual(fixesAndDiagnostics); 125 | }); 126 | 127 | test("removeDuplicatedFixes_allTheSameCode", () => { 128 | const fixesAndDiagnostics: FixAndDiagnostic[] = []; 129 | const codefixes = [makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('addConvertToUnknownForNonOverlappingTypes', 9, 64), 130 | makeCodeFix('fixOverrideModifier', 165, 0), makeCodeFix('fixOverrideModifier', 165, 0), 131 | makeCodeFix('fixOverrideModifier', 165, 0)]; 132 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[0], 435, 4, 5)); 133 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[1], 543, 4, 5)); 134 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[2], 545, 4, 5)); 135 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[3], 435, 6, 5)); 136 | fixesAndDiagnostics.push(makeFixAndDiagnostic(codefixes[4], 212, 4, 5)); 137 | const result = removeMultipleDiagnostics(fixesAndDiagnostics, Choices.ACCEPT); 138 | expect(result).toEqual(fixesAndDiagnostics); 139 | }); 140 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "target": "ES2019", 6 | "module": "Node16", 7 | "lib": ["ES2019"], 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | "outDir": "./dist", 15 | // stricter type-checking for stronger correctness. Recommended by TS 16 | "strict": true, 17 | // linter checks for common issues 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": true, 23 | // use Node's module resolution algorithm, instead of the legacy TS one 24 | // interop between ESM and CJS modules. Recommended by TS 25 | "esModuleInterop": true, 26 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 27 | "skipLibCheck": true, 28 | // error out if import and file system have a casing mismatch. Recommended by TS 29 | "forceConsistentCasingInFileNames": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsdxstart.md: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | 4 | 5 | # TSDX User Guide 6 | 7 | Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. 8 | 9 | > This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. 10 | 11 | > If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) 12 | 13 | ## Commands 14 | 15 | TSDX scaffolds your new library inside `/src`. 16 | 17 | To run TSDX, use: 18 | 19 | ```bash 20 | npm start # or yarn start 21 | ``` 22 | 23 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 24 | 25 | To do a one-off build, use `npm run build` or `yarn build`. 26 | 27 | To run tests, use `npm test` or `yarn test`. 28 | 29 | ## Configuration 30 | 31 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 32 | 33 | ### Jest 34 | 35 | Jest tests are set up to run with `npm test` or `yarn test`. 36 | 37 | ### Bundle Analysis 38 | 39 | [`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`. 40 | 41 | #### Setup Files 42 | 43 | This is the folder structure we set up for you: 44 | 45 | ```txt 46 | /src 47 | index.tsx # EDIT THIS 48 | /test 49 | blah.test.tsx # EDIT THIS 50 | .gitignore 51 | package.json 52 | README.md # EDIT THIS 53 | tsconfig.json 54 | ``` 55 | 56 | ### Rollup 57 | 58 | TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 59 | 60 | ### TypeScript 61 | 62 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 63 | 64 | ## Continuous Integration 65 | 66 | ### GitHub Actions 67 | 68 | Two actions are added by default: 69 | 70 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 71 | - `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) 72 | 73 | ## Optimizations 74 | 75 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: 76 | 77 | ```js 78 | // ./types/index.d.ts 79 | declare var __DEV__: boolean; 80 | 81 | // inside your code... 82 | if (__DEV__) { 83 | console.log('foo'); 84 | } 85 | ``` 86 | 87 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 88 | 89 | ## Module Formats 90 | 91 | CJS, ESModules, and UMD module formats are supported. 92 | 93 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 94 | 95 | ## Named Exports 96 | 97 | Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 98 | 99 | ## Including Styles 100 | 101 | There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. 102 | 103 | For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. 104 | 105 | ## Publishing to NPM 106 | 107 | We recommend using [np](https://github.com/sindresorhus/np). 108 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | silent: true 6 | } 7 | }); --------------------------------------------------------------------------------