diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000..404b4177c9773 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +/built/local/** +/tests/** +/lib/** +/src/lib/*.generated.d.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000000..1f39c29101f83 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,110 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "warnOnUnsupportedTypeScriptVersion": false, + "ecmaVersion": 6, + "sourceType": "module" + }, + "env": { + "browser": false, + "node": true, + "es6": true + }, + "plugins": [ + "@typescript-eslint", "jsdoc", "no-null", "import" + ], + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": "error", + + "camelcase": "off", + "@typescript-eslint/camelcase": ["error", { "properties": "never", "allow": ["^[A-Za-z][a-zA-Za-z]+_[A-Za-z]+$"] }], + + "@typescript-eslint/class-name-casing": "error", + "@typescript-eslint/consistent-type-definitions": ["error", "interface"], + "@typescript-eslint/interface-name-prefix": "error", + "@typescript-eslint/no-inferrable-types": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-this-alias": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/prefer-function-type": "error", + "@typescript-eslint/prefer-namespace-keyword": "error", + + "quotes": "off", + "@typescript-eslint/quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }], + + "semi": "off", + "@typescript-eslint/semi": "error", + + "@typescript-eslint/triple-slash-reference": "error", + "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/unified-signatures": "error", + + // scripts/eslint/rules + "object-literal-surrounding-space": "error", + "no-type-assertion-whitespace": "error", + "type-operator-spacing": "error", + "only-arrow-functions": ["error", { + "allowNamedFunctions": true , + "allowDeclarations": true + }], + "no-double-space": "error", + "boolean-trivia": "error", + "no-in-operator": "error", + "simple-indent": "error", + "debug-assert": "error", + "no-keywords": "error", + + // eslint-plugin-import + "import/no-extraneous-dependencies": ["error", { "optionalDependencies": false }], + + // eslint-plugin-no-null + "no-null/no-null": "error", + + // eslint-plugin-jsdoc + "jsdoc/check-alignment": "error", + + // eslint + "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], + "constructor-super": "error", + "curly": ["error", "multi-line"], + "dot-notation": "error", + "eqeqeq": "error", + "linebreak-style": ["error", "windows"], + "new-parens": "error", + "no-caller": "error", + "no-duplicate-case": "error", + "no-duplicate-imports": "error", + "no-empty": "error", + "no-eval": "error", + "no-extra-bind": "error", + "no-fallthrough": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-return-await": "error", + "no-restricted-globals": ["error", + { "name": "setTimeout" }, + { "name": "clearTimeout" }, + { "name": "setInterval" }, + { "name": "clearInterval" }, + { "name": "setImmediate" }, + { "name": "clearImmediate" } + ], + "no-sparse-arrays": "error", + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef-init": "error", + "no-unsafe-finally": "error", + "no-unused-expressions": ["error", { "allowTernary": true }], + "no-unused-labels": "error", + "no-var": "error", + "object-shorthand": "error", + "prefer-const": "error", + "prefer-object-spread": "error", + "quote-props": ["error", "consistent-as-needed"], + "space-in-parens": "error", + "unicode-bom": ["error", "never"], + "use-isnan": "error" + } +} diff --git a/.gitignore b/.gitignore index a906e2cf7d5d8..a220522b5f0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ tests/cases/**/*.js !tests/cases/docker/*.js/ tests/cases/**/*.js.map *.config +scripts/eslint/built/ scripts/debug.bat scripts/run.bat scripts/word2md.js @@ -60,6 +61,8 @@ internal/ **/.vs **/.vscode !**/.vscode/tasks.json +!**/.vscode/settings.json +!**/.vscode/extensions.json !tests/cases/projects/projectOption/**/node_modules !tests/cases/projects/NodeModulesSearch/**/* !tests/baselines/reference/project/nodeModules*/**/* diff --git a/.npmignore b/.npmignore index 2144451e3e84d..a0769e52a5444 100644 --- a/.npmignore +++ b/.npmignore @@ -9,8 +9,9 @@ netci.groovy scripts src tests -tslint.json Jakefile.js +.eslintrc +.eslintignore .editorconfig .failed-tests .git diff --git a/.travis.yml b/.travis.yml index 2124f3ada33a4..cfb265e189ef3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ branches: install: - npm uninstall typescript --no-save - - npm uninstall tslint --no-save - npm install cache: diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000..bae8c12d07c8a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ], + + "unwantedRecommendations": [ + "ms-vscode.vscode-typescript-tslint-plugin" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..4f481751900d2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "eslint.validate": [ + { + "language": "typescript", + "autoFix": true + } + ], + "eslint.options": { + "rulePaths": ["../scripts/eslint/built/rules/"], + "ext": [".ts"] + }, + "eslint.workingDirectories": ["./src", "./scripts"] +} \ No newline at end of file diff --git a/Gulpfile.js b/Gulpfile.js index be2c4f06a3855..51127f9822d77 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -318,36 +318,68 @@ task("clean-tests").description = "Cleans the outputs for the test infrastructur const watchTests = () => watchProject("src/testRunner", cmdLineOptions); -const buildRules = () => buildProject("scripts/tslint"); -task("build-rules", buildRules); -task("build-rules").description = "Compiles tslint rules to js"; +const buildEslintRules = () => buildProject("scripts/eslint"); +task("build-eslint-rules", buildEslintRules); +task("build-eslint-rules").description = "Compiles eslint rules to js"; -const cleanRules = () => cleanProject("scripts/tslint"); -cleanTasks.push(cleanRules); -task("clean-rules", cleanRules); -task("clean-rules").description = "Cleans the outputs for the lint rules"; +const cleanEslintRules = () => cleanProject("scripts/eslint"); +cleanTasks.push(cleanEslintRules); +task("clean-eslint-rules", cleanEslintRules); +task("clean-eslint-rules").description = "Cleans the outputs for the eslint rules"; + +const runEslintRulesTests = () => runConsoleTests("scripts/eslint/built/tests", "mocha-fivemat-progress-reporter", /*runInParallel*/ false, /*watchMode*/ false); +task("run-eslint-rules-tests", series(buildEslintRules, runEslintRulesTests)); +task("run-eslint-rules-tests").description = "Runs the eslint rule tests"; const lintFoldStart = async () => { if (fold.isTravis()) console.log(fold.start("lint")); }; const lintFoldEnd = async () => { if (fold.isTravis()) console.log(fold.end("lint")); }; -const lint = series([ - lintFoldStart, - ...["scripts/tslint/tsconfig.json", "src/tsconfig-base.json"].map(project => { - const lintOne = () => { - const args = ["node_modules/tslint/bin/tslint", "--project", project, "--formatters-dir", "./built/local/tslint/formatters", "--format", "autolinkableStylish"]; - if (cmdLineOptions.fix) args.push("--fix"); - log(`Linting: node ${args.join(" ")}`); - return exec(process.execPath, args); - }; - lintOne.dispayName = `lint(${project})`; - return lintOne; - }), - lintFoldEnd -]); +const eslint = (folder) => async () => { + const ESLINTRC_CI = ".eslintrc.ci.json"; + const isCIEnv = cmdLineOptions.ci || process.env.CI === "true"; + + const args = [ + "node_modules/eslint/bin/eslint", + "--format", "autolinkable-stylish", + "--rulesdir", "scripts/eslint/built/rules", + "--ext", ".ts", + ]; + + if ( + isCIEnv && + fs.existsSync(path.resolve(folder, ESLINTRC_CI)) + ) { + args.push("--config", path.resolve(folder, ESLINTRC_CI)); + } + + if (cmdLineOptions.fix) { + args.push("--fix"); + } + + args.push(folder); + + log(`Linting: ${args.join(" ")}`); + return exec(process.execPath, args); +} + +const lintScripts = eslint("scripts"); +lintScripts.displayName = "lint-scripts"; +task("lint-scripts", series([buildEslintRules, lintFoldStart, lintScripts, lintFoldEnd])); +task("lint-scripts").description = "Runs eslint on the scripts sources."; + +const lintCompiler = eslint("src"); +lintCompiler.displayName = "lint-compiler"; +task("lint-compiler", series([buildEslintRules, lintFoldStart, lintCompiler, lintFoldEnd])); +task("lint-compiler").description = "Runs eslint on the compiler sources."; +task("lint-compiler").flags = { + " --ci": "Runs eslint additional rules", +}; + +const lint = series([buildEslintRules, lintFoldStart, lintScripts, lintCompiler, lintFoldEnd]); lint.displayName = "lint"; -task("lint", series(buildRules, lint)); -task("lint").description = "Runs tslint on the compiler sources."; +task("lint", series([buildEslintRules, lintFoldStart, lint, lintFoldEnd])); +task("lint").description = "Runs eslint on the compiler and scripts sources."; task("lint").flags = { - " --f[iles]=": "pattern to match files to lint", + " --ci": "Runs eslint additional rules", }; const buildCancellationToken = () => buildProject("src/cancellationToken"); @@ -393,7 +425,7 @@ const generateCodeCoverage = () => exec("istanbul", ["cover", "node_modules/moch task("generate-code-coverage", series(preBuild, buildTests, generateCodeCoverage)); task("generate-code-coverage").description = "Generates code coverage data via istanbul"; -const preTest = parallel(buildRules, buildTests, buildServices, buildLssl); +const preTest = parallel(buildTests, buildServices, buildLssl); preTest.displayName = "preTest"; const postTest = (done) => cmdLineOptions.lint ? lint(done) : done(); diff --git a/README.md b/README.md index 7dff5ec1be674..c1bf05b11b657 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ gulp runtests # Run tests using the built compiler and test infrastruct gulp runtests-parallel # Like runtests, but split across multiple threads. Uses a number of threads equal to the system # core count by default. Use --workers= to adjust this. gulp baseline-accept # This replaces the baseline test results with the results obtained from gulp runtests. -gulp lint # Runs tslint on the TypeScript source. +gulp lint # Runs eslint on the TypeScript source. gulp help # List the above commands. ``` diff --git a/package.json b/package.json index 37ccd6cf71c89..478538926ce94 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "@types/browserify": "latest", "@types/chai": "latest", "@types/convert-source-map": "latest", - "@types/del": "latest", "@types/glob": "latest", "@types/gulp": "^4.0.5", "@types/gulp-concat": "latest", @@ -55,15 +54,25 @@ "@types/through2": "latest", "@types/travis-fold": "latest", "@types/xml2js": "^0.4.0", + "@typescript-eslint/eslint-plugin": "2.2.0", + "@typescript-eslint/experimental-utils": "2.2.0", + "@typescript-eslint/parser": "2.2.0", + "async": "latest", "azure-devops-node-api": "^8.0.0", "browser-resolve": "^1.11.2", "browserify": "latest", "chai": "latest", "chalk": "latest", "convert-source-map": "latest", - "del": "latest", + "del": "5.1.0", + "eslint": "6.3.0", + "eslint-formatter-autolinkable-stylish": "1.0.3", + "eslint-plugin-import": "2.18.2", + "eslint-plugin-jsdoc": "15.9.1", + "eslint-plugin-no-null": "1.0.2", "fancy-log": "latest", "fs-extra": "^6.0.1", + "glob": "latest", "gulp": "^4.0.0", "gulp-concat": "latest", "gulp-insert": "latest", @@ -87,7 +96,6 @@ "source-map-support": "latest", "through2": "latest", "travis-fold": "latest", - "tslint": "latest", "typescript": "next", "vinyl": "latest", "vinyl-sourcemaps-apply": "latest", @@ -96,6 +104,7 @@ "scripts": { "pretest": "gulp tests", "test": "gulp runtests-parallel --light=false", + "test:eslint-rules": "gulp run-eslint-rules-tests", "build": "npm run build:compiler && npm run build:tests", "build:compiler": "gulp local", "build:tests": "gulp tests", @@ -104,6 +113,9 @@ "gulp": "gulp", "jake": "gulp", "lint": "gulp lint", + "lint:ci": "gulp lint --ci", + "lint:compiler": "gulp lint-compiler", + "lint:scripts": "gulp lint-scripts", "setup-hooks": "node scripts/link-hooks.js", "update-costly-tests": "node scripts/costly-tests.js" }, diff --git a/scripts/authors.ts b/scripts/authors.ts index f92511ed6efdb..0bcfc42579063 100644 --- a/scripts/authors.ts +++ b/scripts/authors.ts @@ -1,19 +1,21 @@ -import fs = require('fs'); -import path = require('path'); -import child_process = require("child_process"); +import fs = require("fs"); +import path = require("path"); +import childProcess = require("child_process"); -type Author = { +interface Author { displayNames: string[]; preferredName?: string; emails: string[]; -}; +} -type AuthorMap = { [s: string]: Author }; +interface AuthorMap { + [s: string]: Author +} -type Command = { +interface Command { (...arg: string[]): void; description?: string; -}; +} const mailMapPath = path.resolve(__dirname, "../.mailmap"); const authorsPath = path.resolve(__dirname, "../AUTHORS.md"); @@ -71,7 +73,7 @@ function getKnownAuthorMaps() { } function deduplicate(array: T[]): T[] { - let result: T[] = [] + const result: T[] = []; if (array) { for (const item of array) { if (result.indexOf(item) < 0) { @@ -98,23 +100,23 @@ function sortAuthors(a: string, b: string) { } namespace Commands { - export const writeAuthors: Command = function () { + export const writeAuthors: Command = () => { const output = deduplicate(getKnownAuthors().map(getAuthorName).filter(a => !!a)).sort(sortAuthors).join("\r\n* "); fs.writeFileSync(authorsPath, "TypeScript is authored by:\r\n* " + output); }; writeAuthors.description = "Write known authors to AUTHORS.md file."; - export const listKnownAuthors: Command = function () { + export const listKnownAuthors: Command = () => { deduplicate(getKnownAuthors().map(getAuthorName)).filter(a => !!a).sort(sortAuthors).forEach(log); }; listKnownAuthors.description = "List known authors as listed in .mailmap file."; - export const listAuthors: Command = function (...specs:string[]) { + export const listAuthors: Command = (...specs: string[]) => { const cmd = "git shortlog -se " + specs.join(" "); console.log(cmd); const outputRegExp = /\d+\s+([^<]+)<([^>]+)>/; const authors: { name: string, email: string, knownAuthor?: Author }[] = []; - const {output: [error, stdout, stderr]} = child_process.spawnSync(`git`, ["shortlog", "-se", ...specs], { cwd: path.resolve(__dirname, "../") }); + const {output: [error, stdout, stderr]} = childProcess.spawnSync(`git`, ["shortlog", "-se", ...specs], { cwd: path.resolve(__dirname, "../") }); if (error) { console.log(stderr.toString()); } @@ -135,7 +137,7 @@ namespace Commands { const maps = getKnownAuthorMaps(); - const lookupAuthor = function ({name, email}: { name: string, email: string }) { + const lookupAuthor = ({name, email}: { name: string, email: string }) => { return maps.authorsByEmail[email.toLocaleLowerCase()] || maps.authorsByName[name]; }; @@ -165,16 +167,18 @@ namespace Commands { listAuthors.description = "List known and unknown authors for a given spec, e.g. 'node authors.js listAuthors origin/release-2.6..origin/release-2.7'"; } -var args = process.argv.slice(2); +const args = process.argv.slice(2); if (args.length < 1) { - console.log('Usage: node authors.js [command]'); - console.log('List of commands: '); - Object.keys(Commands).forEach(k => console.log(` ${k}: ${(Commands as any)[k]['description']}`)); -} else { - var cmd: Function = (Commands as any)[args[0]]; + console.log("Usage: node authors.js [command]"); + console.log("List of commands: "); + Object.keys(Commands).forEach(k => console.log(` ${k}: ${(Commands as any)[k].description}`)); +} +else { + const cmd: Function = (Commands as any)[args[0]]; if (cmd === undefined) { - console.log('Unknown command ' + args[1]); - } else { + console.log("Unknown command " + args[1]); + } + else { cmd.apply(undefined, args.slice(1)); } } diff --git a/scripts/bisect-test.ts b/scripts/bisect-test.ts index d66e0b7106132..a3e48b29a81f5 100644 --- a/scripts/bisect-test.ts +++ b/scripts/bisect-test.ts @@ -2,58 +2,62 @@ * You should have ts-node installed globally before executing this, probably! * Otherwise you'll need to compile this script before you start bisecting! */ -import cp = require('child_process'); -import fs = require('fs'); +import cp = require("child_process"); +import fs = require("fs"); // Slice off 'node bisect-test.js' from the command line args -var args = process.argv.slice(2); +const args = process.argv.slice(2); function tsc(tscArgs: string, onExit: (exitCode: number) => void) { - var tsc = cp.exec('node built/local/tsc.js ' + tscArgs,() => void 0); - tsc.on('close', tscExitCode => { + const tsc = cp.exec("node built/local/tsc.js " + tscArgs,() => void 0); + tsc.on("close", tscExitCode => { onExit(tscExitCode); }); } // TODO: Rewrite bisect script to handle the post-jake/gulp swap period -var jake = cp.exec('jake clean local', () => void 0); -jake.on('close', jakeExitCode => { +const jake = cp.exec("jake clean local", () => void 0); +jake.on("close", jakeExitCode => { if (jakeExitCode === 0) { // See what we're being asked to do - if (args[1] === 'compiles' || args[1] === '!compiles') { + if (args[1] === "compiles" || args[1] === "!compiles") { tsc(args[0], tscExitCode => { - if ((tscExitCode === 0) === (args[1] === 'compiles')) { - console.log('Good'); + if ((tscExitCode === 0) === (args[1] === "compiles")) { + console.log("Good"); process.exit(0); // Good - } else { - console.log('Bad'); + } + else { + console.log("Bad"); process.exit(1); // Bad } }); - } else if (args[1] === 'emits' || args[1] === '!emits') { + } + else if (args[1] === "emits" || args[1] === "!emits") { tsc(args[0], tscExitCode => { - fs.readFile(args[2], 'utf-8', (err, data) => { - var doesContains = data.indexOf(args[3]) >= 0; - if (doesContains === (args[1] === 'emits')) { - console.log('Good'); + fs.readFile(args[2], "utf-8", (err, data) => { + const doesContains = data.indexOf(args[3]) >= 0; + if (doesContains === (args[1] === "emits")) { + console.log("Good"); process.exit(0); // Good - } else { - console.log('Bad'); + } + else { + console.log("Bad"); process.exit(1); // Bad } }); }); - } else { - console.log('Unknown command line arguments.'); - console.log('Usage (compile errors): git bisect run ts-node scripts\bisect-test.ts "../failure.ts --module amd" !compiles'); - console.log('Usage (emit check): git bisect run ts-node scripts\bisect-test.ts bar.ts emits bar.js "_this = this"'); + } + else { + console.log("Unknown command line arguments."); + console.log("Usage (compile errors): git bisect run ts-node scripts\bisect-test.ts '../failure.ts --module amd' !compiles"); + console.log("Usage (emit check): git bisect run ts-node scripts\bisect-test.ts bar.ts emits bar.js '_this = this'"); // Aborts the 'git bisect run' process process.exit(-1); } - } else { + } + else { // Compiler build failed; skip this commit - console.log('Skip'); + console.log("Skip"); process.exit(125); // bisect skip } }); - \ No newline at end of file diff --git a/scripts/buildProtocol.ts b/scripts/buildProtocol.ts index 006608323cf1f..ad29741287c9c 100644 --- a/scripts/buildProtocol.ts +++ b/scripts/buildProtocol.ts @@ -20,19 +20,19 @@ class DeclarationsWalker { } static getExtraDeclarations(typeChecker: ts.TypeChecker, protocolFile: ts.SourceFile): string { + const walker = new DeclarationsWalker(typeChecker, protocolFile); let text = "declare namespace ts.server.protocol {\n"; - var walker = new DeclarationsWalker(typeChecker, protocolFile); walker.visitTypeNodes(protocolFile); text = walker.text ? `declare namespace ts.server.protocol {\n${walker.text}}` : ""; if (walker.removedTypes) { text += "\ndeclare namespace ts {\n"; - text += " // these types are empty stubs for types from services and should not be used directly\n" + text += " // these types are empty stubs for types from services and should not be used directly\n"; for (const type of walker.removedTypes) { - text += ` export type ${type.symbol!.name} = never;\n`; + text += ` export type ${type.symbol.name} = never;\n`; } - text += "}" + text += "}"; } return text; } @@ -42,7 +42,7 @@ class DeclarationsWalker { return; } this.visitedTypes.push(type); - let s = type.aliasSymbol || type.getSymbol(); + const s = type.aliasSymbol || type.getSymbol(); if (!s) { return; } @@ -64,7 +64,7 @@ class DeclarationsWalker { } else { // splice declaration in final d.ts file - let text = decl.getFullText(); + const text = decl.getFullText(); this.text += `${text}\n`; // recursively pull all dependencies into result dts file @@ -139,10 +139,10 @@ function writeProtocolFile(outputFile: string, protocolTs: string, typeScriptSer if (protocolDts === undefined) { const diagHost: ts.FormatDiagnosticsHost = { - getCanonicalFileName: function (f) { return f; }, - getCurrentDirectory: function() { return '.'; }, - getNewLine: function() { return "\r\n"; } - } + getCanonicalFileName(f) { return f; }, + getCurrentDirectory() { return "."; }, + getNewLine() { return "\r\n"; } + }; const diags = emitResult.diagnostics.map(d => ts.formatDiagnostic(d, diagHost)).join("\r\n"); throw new Error(`Declaration file for protocol.ts is not generated:\r\n${diags}`); } @@ -161,7 +161,7 @@ function writeProtocolFile(outputFile: string, protocolTs: string, typeScriptSer return ts.createSourceFile(fileName, protocolDts, options.target); } return originalGetSourceFile.apply(host, [fileName]); - } + }; const rootFiles = includeTypeScriptServices ? [protocolFileName, typeScriptServicesDts] : [protocolFileName]; return ts.createProgram(rootFiles, options, host); } diff --git a/scripts/configurePrerelease.ts b/scripts/configurePrerelease.ts index 5e3ac5653d72e..e877019d19b85 100644 --- a/scripts/configurePrerelease.ts +++ b/scripts/configurePrerelease.ts @@ -48,10 +48,11 @@ function main(): void { // Finally write the changes to disk. // Modify the package.json structure packageJsonValue.version = `${majorMinor}.${prereleasePatch}`; - writeFileSync(packageJsonFilePath, JSON.stringify(packageJsonValue, /*replacer:*/ undefined, /*space:*/ 4)) + writeFileSync(packageJsonFilePath, JSON.stringify(packageJsonValue, /*replacer:*/ undefined, /*space:*/ 4)); writeFileSync(tsFilePath, modifiedTsFileContents); } +/* eslint-disable no-null/no-null */ function updateTsFile(tsFilePath: string, tsFileContents: string, majorMinor: string, patch: string, nightlyPatch: string): string { const majorMinorRgx = /export const versionMajorMinor = "(\d+\.\d+)"/; const majorMinorMatch = majorMinorRgx.exec(tsFileContents); @@ -76,6 +77,7 @@ function parsePackageJsonVersion(versionString: string): { majorMinor: string, p assert(match !== null, "package.json 'version' should match " + versionRgx.toString()); return { majorMinor: match![1], patch: match![2] }; } +/* eslint-enable no-null/no-null */ /** e.g. 0-dev.20170707 */ function getPrereleasePatch(tag: string, plainPatch: string): string { diff --git a/scripts/errorCheck.ts b/scripts/errorCheck.ts index a5cb27417f16e..dfd78724115c6 100644 --- a/scripts/errorCheck.ts +++ b/scripts/errorCheck.ts @@ -1,36 +1,35 @@ -declare var require: any; -let fs = require('fs'); -let async = require('async'); -let glob = require('glob'); +const fs = require("fs"); +const async = require("async"); +const glob = require("glob"); -fs.readFile('src/compiler/diagnosticMessages.json', 'utf-8', (err, data) => { +fs.readFile("src/compiler/diagnosticMessages.json", "utf-8", (err, data) => { if (err) { throw err; } - let messages = JSON.parse(data); - let keys = Object.keys(messages); - console.log('Loaded ' + keys.length + ' errors'); + const messages = JSON.parse(data); + const keys = Object.keys(messages); + console.log("Loaded " + keys.length + " errors"); - for (let k of keys) { - messages[k]['seen'] = false; + for (const k of keys) { + messages[k].seen = false; } - let errRegex = /\(\d+,\d+\): error TS([^:]+):/g; + const errRegex = /\(\d+,\d+\): error TS([^:]+):/g; + const baseDir = "tests/baselines/reference/"; - let baseDir = 'tests/baselines/reference/'; fs.readdir(baseDir, (err, files) => { - files = files.filter(f => f.indexOf('.errors.txt') > 0); - let tasks: Array<(callback: () => void) => void> = []; + files = files.filter(f => f.indexOf(".errors.txt") > 0); + const tasks: ((callback: () => void) => void)[] = []; files.forEach(f => tasks.push(done => { - fs.readFile(baseDir + f, 'utf-8', (err, baseline) => { + fs.readFile(baseDir + f, "utf-8", (err, baseline) => { if (err) throw err; let g: string[]; while (g = errRegex.exec(baseline)) { - var errCode = +g[1]; - let msg = keys.filter(k => messages[k].code === errCode)[0]; - messages[msg]['seen'] = true; + const errCode = +g[1]; + const msg = keys.filter(k => messages[k].code === errCode)[0]; + messages[msg].seen = true; } done(); @@ -38,49 +37,49 @@ fs.readFile('src/compiler/diagnosticMessages.json', 'utf-8', (err, data) => { })); async.parallelLimit(tasks, 25, done => { - console.log('== List of errors not present in baselines =='); + console.log("== List of errors not present in baselines =="); let count = 0; - for (let k of keys) { - if (messages[k]['seen'] !== true) { + for (const k of keys) { + if (messages[k].seen !== true) { console.log(k); count++; } } - console.log(count + ' of ' + keys.length + ' errors are not in baselines'); + console.log(count + " of " + keys.length + " errors are not in baselines"); }); }); }); -fs.readFile('src/compiler/diagnosticInformationMap.generated.ts', 'utf-8', (err, data) => { - let errorRegexp = /\s(\w+): \{ code/g; - let errorNames: string[] = []; +fs.readFile("src/compiler/diagnosticInformationMap.generated.ts", "utf-8", (err, data) => { + const errorRegexp = /\s(\w+): \{ code/g; + const errorNames: string[] = []; let errMatch: string[]; while (errMatch = errorRegexp.exec(data)) { errorNames.push(errMatch[1]); } - let allSrc: string = ''; - glob('./src/**/*.ts', {}, (err, files) => { - console.log('Reading ' + files.length + ' source files'); - for (let file of files) { - if (file.indexOf('diagnosticInformationMap.generated.ts') > 0) { + let allSrc = ""; + glob("./src/**/*.ts", {}, (err, files) => { + console.log("Reading " + files.length + " source files"); + for (const file of files) { + if (file.indexOf("diagnosticInformationMap.generated.ts") > 0) { continue; } - let src = fs.readFileSync(file, 'utf-8'); + const src = fs.readFileSync(file, "utf-8"); allSrc = allSrc + src; } - console.log('Consumed ' + allSrc.length + ' characters of source'); + console.log("Consumed " + allSrc.length + " characters of source"); let count = 0; - console.log('== List of errors not used in source =='); - for (let errName of errorNames) { + console.log("== List of errors not used in source =="); + for (const errName of errorNames) { if (allSrc.indexOf(errName) < 0) { console.log(errName); count++; } } - console.log(count + ' of ' + errorNames.length + ' errors are not used in source'); + console.log(count + " of " + errorNames.length + " errors are not used in source"); }); }); diff --git a/scripts/eslint/rules/boolean-trivia.ts b/scripts/eslint/rules/boolean-trivia.ts new file mode 100644 index 0000000000000..a611e73be5389 --- /dev/null +++ b/scripts/eslint/rules/boolean-trivia.ts @@ -0,0 +1,106 @@ +import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "boolean-trivia", + meta: { + docs: { + description: ``, + category: "Best Practices", + recommended: "error", + }, + messages: { + booleanTriviaArgumentError: `Tag argument with parameter name`, + booleanTriviaArgumentSpaceError: `There should be 1 space between an argument and its comment`, + }, + schema: [], + type: "problem", + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.getSourceCode(); + const sourceCodeText = sourceCode.getText(); + + const isSetOrAssert = (name: string): boolean => name.startsWith("set") || name.startsWith("assert"); + const isTrivia = (node: TSESTree.Expression): boolean => { + if (node.type === AST_NODE_TYPES.Identifier) { + return node.name === "undefined"; + } + + if (node.type === AST_NODE_TYPES.Literal) { + // eslint-disable-next-line no-null/no-null + return node.value === null || node.value === true || node.value === false; + } + + return false; + }; + + const shouldIgnoreCalledExpression = (node: TSESTree.CallExpression): boolean => { + if (node.callee && node.callee.type === AST_NODE_TYPES.MemberExpression) { + const methodName = node.callee.property.type === AST_NODE_TYPES.Identifier + ? node.callee.property.name + : ""; + + if (isSetOrAssert(methodName)) { + return true; + } + + return ["apply", "call", "equal", "fail", "isTrue", "output", "stringify", "push"].indexOf(methodName) >= 0; + } + + if (node.callee && node.callee.type === AST_NODE_TYPES.Identifier) { + const functionName = node.callee.name; + + if (isSetOrAssert(functionName)) { + return true; + } + + return [ + "createImportSpecifier", + "createAnonymousType", + "createSignature", + "createProperty", + "resolveName", + "contains", + ].indexOf(functionName) >= 0; + } + + return false; + }; + + const checkArg = (node: TSESTree.Expression): void => { + if (!isTrivia(node)) { + return; + } + + const comments = sourceCode.getCommentsBefore(node); + if (!comments || comments.length !== 1 || comments[0].type !== "Block") { + context.report({ messageId: "booleanTriviaArgumentError", node }); + return; + } + + const argRangeStart = node.range[0]; + const commentRangeEnd = comments[0].range[1]; + const hasNewLine = sourceCodeText.slice(commentRangeEnd, argRangeStart).indexOf("\n") >= 0; + + if (argRangeStart !== commentRangeEnd + 1 && !hasNewLine) { + context.report({ messageId: "booleanTriviaArgumentSpaceError", node }); + } + }; + + const checkBooleanTrivia = (node: TSESTree.CallExpression) => { + if (shouldIgnoreCalledExpression(node)) { + return; + } + + for (const arg of node.arguments) { + checkArg(arg); + } + }; + + return { + CallExpression: checkBooleanTrivia, + }; + }, +}); diff --git a/scripts/eslint/rules/debug-assert.ts b/scripts/eslint/rules/debug-assert.ts new file mode 100644 index 0000000000000..0e8769f1fe5b1 --- /dev/null +++ b/scripts/eslint/rules/debug-assert.ts @@ -0,0 +1,60 @@ +import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "debug-assert", + meta: { + docs: { + description: ``, + category: "Possible Errors", + recommended: "error", + }, + messages: { + secondArgumentDebugAssertError: `Second argument to 'Debug.assert' should be a string literal`, + thirdArgumentDebugAssertError: `Third argument to 'Debug.assert' should be a string literal or arrow function`, + }, + schema: [], + type: "problem", + }, + defaultOptions: [], + + create(context) { + const isArrowFunction = (node: TSESTree.Node) => node.type === AST_NODE_TYPES.ArrowFunctionExpression; + const isStringLiteral = (node: TSESTree.Node): boolean => ( + (node.type === AST_NODE_TYPES.Literal && typeof node.value === "string") || node.type === AST_NODE_TYPES.TemplateLiteral + ); + + const isDebugAssert = (node: TSESTree.MemberExpression): boolean => ( + node.object.type === AST_NODE_TYPES.Identifier + && node.object.name === "Debug" + && node.property.type === AST_NODE_TYPES.Identifier + && node.property.name === "assert" + ); + + const checkDebugAssert = (node: TSESTree.CallExpression) => { + const args = node.arguments; + const argsLen = args.length; + if (!(node.callee.type === AST_NODE_TYPES.MemberExpression && isDebugAssert(node.callee)) || argsLen < 2) { + return; + } + + const message1Node = args[1]; + if (message1Node && !isStringLiteral(message1Node)) { + context.report({ messageId: "secondArgumentDebugAssertError", node: message1Node }); + } + + if (argsLen < 3) { + return; + } + + const message2Node = args[2]; + if (message2Node && (!isStringLiteral(message2Node) && !isArrowFunction(message2Node))) { + context.report({ messageId: "thirdArgumentDebugAssertError", node: message2Node }); + } + }; + + return { + CallExpression: checkDebugAssert, + }; + }, +}); diff --git a/scripts/eslint/rules/no-double-space.ts b/scripts/eslint/rules/no-double-space.ts new file mode 100644 index 0000000000000..7884c4c108b2f --- /dev/null +++ b/scripts/eslint/rules/no-double-space.ts @@ -0,0 +1,71 @@ +import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "no-double-space", + meta: { + docs: { + description: ``, + category: "Stylistic Issues", + recommended: "error", + }, + messages: { + noDoubleSpaceError: `Use only one space`, + }, + schema: [], + type: "problem", + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.getSourceCode(); + const lines = sourceCode.getLines(); + + const isStringLiteral = (node: TSESTree.Node | null): boolean => { + return !!(node && ( + (node.type === AST_NODE_TYPES.TemplateElement) || + (node.type === AST_NODE_TYPES.TemplateLiteral && node.quasis) || + (node.type === AST_NODE_TYPES.Literal && typeof node.value === "string") + )); + }; + + const isRegexLiteral = (node: TSESTree.Node | null): boolean => { + return !!(node && node.type === AST_NODE_TYPES.Literal && node.regex); + }; + + const checkDoubleSpace = (node: TSESTree.Node) => { + lines.forEach((line, index) => { + const firstNonSpace = /\S/.exec(line); + if (!firstNonSpace || line.includes("@param")) { + return; + } + + // Allow common uses of double spaces + // * To align `=` or `!=` signs + // * To align comments at the end of lines + // * To indent inside a comment + // * To use two spaces after a period + // * To include aligned `->` in a comment + const rgx = /[^/*. ][ ]{2}[^-!/= ]/g; + rgx.lastIndex = firstNonSpace.index; + const doubleSpace = rgx.exec(line); + + if (!doubleSpace) { + return; + } + + const locIndex = sourceCode.getIndexFromLoc({ column: doubleSpace.index, line: index + 1 }); + const sourceNode = sourceCode.getNodeByRangeIndex(locIndex); + if (isStringLiteral(sourceNode) || isRegexLiteral(sourceNode)) { + return; + } + + context.report({ messageId: "noDoubleSpaceError", node, loc: { line: index + 1, column: doubleSpace.index + 1 } }); + }); + }; + + return { + Program: checkDoubleSpace, + }; + }, +}); diff --git a/scripts/eslint/rules/no-in-operator.ts b/scripts/eslint/rules/no-in-operator.ts new file mode 100644 index 0000000000000..99bf5fbeb7ba6 --- /dev/null +++ b/scripts/eslint/rules/no-in-operator.ts @@ -0,0 +1,32 @@ +import { TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "no-in-operator", + meta: { + docs: { + description: ``, + category: "Best Practices", + recommended: "error", + }, + messages: { + noInOperatorError: `Don't use the 'in' keyword - use 'hasProperty' to check for key presence instead`, + }, + schema: [], + type: "suggestion", + }, + defaultOptions: [], + + create(context) { + const IN_OPERATOR = "in"; + const checkInOperator = (node: TSESTree.BinaryExpression) => { + if (node.operator === IN_OPERATOR) { + context.report({ messageId: "noInOperatorError", node }); + } + }; + + return { + BinaryExpression: checkInOperator, + }; + }, +}); diff --git a/scripts/eslint/rules/no-keywords.ts b/scripts/eslint/rules/no-keywords.ts new file mode 100644 index 0000000000000..891806666470e --- /dev/null +++ b/scripts/eslint/rules/no-keywords.ts @@ -0,0 +1,118 @@ +import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "no-keywords", + meta: { + docs: { + description: `disallows the use of certain TypeScript keywords as variable or parameter names`, + category: "Stylistic Issues", + recommended: "error", + }, + messages: { + noKeywordsError: `{{ name }} clashes with keyword/type`, + }, + schema: [{ + properties: { + properties: { type: "boolean" }, + keywords: { type: "boolean" }, + }, + type: "object", + }], + type: "suggestion", + }, + defaultOptions: [], + + create(context) { + const keywords = [ + "Undefined", + "undefined", + "Boolean", + "boolean", + "String", + "string", + "Number", + "number", + "any", + ]; + + const isKeyword = (name: string) => keywords.includes(name); + + const report = (node: TSESTree.Identifier) => { + context.report({ messageId: "noKeywordsError", data: { name: node.name }, node }); + }; + + const checkProperties = (node: TSESTree.ObjectPattern): void => { + node.properties.forEach(property => { + if ( + property && + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + isKeyword(property.key.name) + ) { + report(property.key); + } + }); + }; + + const checkElements = (node: TSESTree.ArrayPattern): void => { + node.elements.forEach(element => { + if ( + element && + element.type === AST_NODE_TYPES.Identifier && + isKeyword(element.name) + ) { + report(element); + } + }); + }; + + const checkParams = ( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.TSMethodSignature + | TSESTree.TSFunctionType + ): void => { + if (!node || !node.params || !node.params.length) { + return; + } + + node.params.forEach(param => { + if ( + param && + param.type === AST_NODE_TYPES.Identifier && + isKeyword(param.name) + ) { + report(param); + } + }); + }; + + return { + VariableDeclarator(node) { + if (node.id.type === AST_NODE_TYPES.ObjectPattern) { + checkProperties(node.id); + } + + if (node.id.type === AST_NODE_TYPES.ArrayPattern) { + checkElements(node.id); + } + + if ( + node.id.type === AST_NODE_TYPES.Identifier && + isKeyword(node.id.name) + ) { + report(node.id); + } + }, + + ArrowFunctionExpression: checkParams, + FunctionDeclaration: checkParams, + FunctionExpression: checkParams, + TSMethodSignature: checkParams, + TSFunctionType: checkParams, + }; + }, +}); diff --git a/scripts/eslint/rules/no-type-assertion-whitespace.ts b/scripts/eslint/rules/no-type-assertion-whitespace.ts new file mode 100644 index 0000000000000..339c7c86ff227 --- /dev/null +++ b/scripts/eslint/rules/no-type-assertion-whitespace.ts @@ -0,0 +1,43 @@ +import { TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "no-type-assertion-whitespace", + meta: { + docs: { + description: ``, + category: "Stylistic Issues", + recommended: "error", + }, + messages: { + noTypeAssertionWhitespace: `Excess trailing whitespace found around type assertion`, + }, + schema: [], + type: "problem", + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.getSourceCode(); + const checkTypeAssertionWhitespace = (node: TSESTree.TSTypeAssertion) => { + const leftToken = sourceCode.getLastToken(node.typeAnnotation); + const rightToken = sourceCode.getFirstToken(node.expression); + + if (!leftToken || !rightToken) { + return; + } + + if (sourceCode.isSpaceBetweenTokens(leftToken, rightToken)) { + context.report({ + messageId: "noTypeAssertionWhitespace", + node, + loc: { column: leftToken.loc.end.column + 1, line: leftToken.loc.end.line }, + }); + } + }; + + return { + TSTypeAssertion: checkTypeAssertionWhitespace, + }; + }, +}); diff --git a/scripts/eslint/rules/object-literal-surrounding-space.ts b/scripts/eslint/rules/object-literal-surrounding-space.ts new file mode 100644 index 0000000000000..6cfea3cfac707 --- /dev/null +++ b/scripts/eslint/rules/object-literal-surrounding-space.ts @@ -0,0 +1,71 @@ +import { TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "object-literal-surrounding-space", + meta: { + docs: { + description: ``, + category: "Stylistic Issues", + recommended: "error", + }, + messages: { + leadingExcessStringError: `No trailing whitespace found on single-line object literal`, + leadingStringError: `No leading whitespace found on single-line object literal`, + + trailingExcessStringError: `Excess trailing whitespace found on single-line object literal.`, + trailingStringError: `No trailing whitespace found on single-line object literal`, + }, + schema: [], + type: "suggestion", + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.getSourceCode(); + const SPACE_SYMBOL = " "; + const CLOSE_SYMBOL = "}"; + const OPEN_SYMBOL = "{"; + + const manySpaces = (text: string, startIndex: number): boolean => ( + [startIndex, startIndex + 1].every(i => text.charAt(i) === SPACE_SYMBOL) + ); + + const checkObjectLiteralSurroundingSpace = (node: TSESTree.ObjectExpression) => { + const text = sourceCode.getText(node); + const startLine = node.loc.start.line; + const endLine = node.loc.end.line; + + if (!node.properties.length || !text.match(/^{[^\n]+}$/g)) { + return; + } + + if (text.charAt(0) === OPEN_SYMBOL) { + if (text.charAt(1) !== SPACE_SYMBOL) { + context.report({ messageId: "leadingStringError", node }); + } + + if (manySpaces(text, 1)) { + context.report({ messageId: "leadingExcessStringError", node, loc: { column: node.loc.start.column + 1, line: startLine } }); + } + } + + if (text.charAt(text.length - 1) === CLOSE_SYMBOL) { + const index = text.length - 2; + const endColumn = node.loc.end.column; + + if (text.charAt(index) !== SPACE_SYMBOL) { + context.report({ messageId: "trailingStringError", node, loc: { column: endColumn - 1, line: endLine } }); + } + + if (manySpaces(text, index - 1)) { + context.report({ messageId: "trailingExcessStringError", node, loc: { column: endColumn - 2, line: endLine } }); + } + } + }; + + return { + ObjectExpression: checkObjectLiteralSurroundingSpace, + }; + }, +}); diff --git a/scripts/eslint/rules/only-arrow-functions.ts b/scripts/eslint/rules/only-arrow-functions.ts new file mode 100644 index 0000000000000..feac5a82fcdd5 --- /dev/null +++ b/scripts/eslint/rules/only-arrow-functions.ts @@ -0,0 +1,91 @@ +import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +type MessageId = "onlyArrowFunctionsError"; +type Options = [{ + allowNamedFunctions?: boolean; + allowDeclarations?: boolean; +}]; + +export = createRule({ + name: "only-arrow-functions", + meta: { + docs: { + description: `Disallows traditional (non-arrow) function expressions.`, + category: "Best Practices", + recommended: "error", + }, + messages: { + onlyArrowFunctionsError: "non-arrow functions are forbidden", + }, + schema: [{ + additionalProperties: false, + properties: { + allowNamedFunctions: { type: "boolean" }, + allowDeclarations: { type: "boolean" }, + }, + type: "object", + }], + type: "suggestion", + }, + defaultOptions: [{ + allowNamedFunctions: false, + allowDeclarations: false, + }], + + create(context, [{ allowNamedFunctions, allowDeclarations }]) { + + const isThisParameter = (node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression) => ( + node.params.length && !!node.params.find(param => param.type === AST_NODE_TYPES.Identifier && param.name === "this") + ); + + const isMethodType = (node: TSESTree.Node) => { + const types = [ + AST_NODE_TYPES.MethodDefinition, + AST_NODE_TYPES.Property, + ]; + + const parent = node.parent; + if (!parent) { + return false; + } + + return node.type === AST_NODE_TYPES.FunctionExpression && types.includes(parent.type); + }; + + const stack: boolean[] = []; + const enterFunction = () => { + stack.push(false); + }; + + const markThisUsed = () => { + if (stack.length) { + stack[stack.length - 1] = true; + } + }; + + const exitFunction = (node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression) => { + const methodUsesThis = stack.pop(); + + if (node.type === AST_NODE_TYPES.FunctionDeclaration && allowDeclarations) { + return; + } + + if ((allowNamedFunctions && node.id !== null) || isMethodType(node)) { // eslint-disable-line no-null/no-null + return; + } + + if (!(node.generator || methodUsesThis || isThisParameter(node))) { + context.report({ messageId: "onlyArrowFunctionsError", node }); + } + }; + + return { + "FunctionDeclaration": enterFunction, + "FunctionDeclaration:exit": exitFunction, + "FunctionExpression": enterFunction, + "FunctionExpression:exit": exitFunction, + "ThisExpression": markThisUsed, + }; + }, +}); diff --git a/scripts/eslint/rules/simple-indent.ts b/scripts/eslint/rules/simple-indent.ts new file mode 100644 index 0000000000000..157e8a6910de6 --- /dev/null +++ b/scripts/eslint/rules/simple-indent.ts @@ -0,0 +1,69 @@ +import { TSESTree } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "simple-indent", + meta: { + docs: { + description: "Enforce consistent indentation", + category: "Stylistic Issues", + recommended: "error", + }, + messages: { + simpleIndentError: "4 space indentation expected", + }, + fixable: "whitespace", + schema: [], + type: "layout", + }, + defaultOptions: [], + create(context) { + const TAB_SIZE = 4; + const TAB_REGEX = /\t/g; + const sourceCode = context.getSourceCode(); + const linebreaks = sourceCode.getText().match(/\r\n|[\r\n\u2028\u2029]/gu); + + const checkIndent = (node: TSESTree.Program) => { + const lines = sourceCode.getLines(); + const linesLen = lines.length; + + let totalLen = 0; + for (let i = 0; i < linesLen; i++) { + const lineNumber = i + 1; + const line = lines[i]; + const linebreaksLen = linebreaks && linebreaks[i] ? linebreaks[i].length : 1; + const lineLen = line.length + linebreaksLen; + const matches = /\S/.exec(line); + + if (matches && matches.index) { + const indentEnd = matches.index; + const whitespace = line.slice(0, indentEnd); + + if (!TAB_REGEX.test(whitespace)) { + totalLen += lineLen; + continue; + } + + context.report({ + messageId: "simpleIndentError", + node, + loc: { column: indentEnd, line: lineNumber }, + fix(fixer) { + const rangeStart = totalLen; + const rangeEnd = rangeStart + indentEnd; + + return fixer + .replaceTextRange([rangeStart, rangeEnd], whitespace.replace(TAB_REGEX, " ".repeat(TAB_SIZE))); + } + }); + } + + totalLen += lineLen; + } + }; + + return { + Program: checkIndent, + }; + }, +}); diff --git a/scripts/eslint/rules/type-operator-spacing.ts b/scripts/eslint/rules/type-operator-spacing.ts new file mode 100644 index 0000000000000..4c6dad00f4c20 --- /dev/null +++ b/scripts/eslint/rules/type-operator-spacing.ts @@ -0,0 +1,44 @@ +import { TSESTree, AST_TOKEN_TYPES } from "@typescript-eslint/experimental-utils"; +import { createRule } from "./utils"; + +export = createRule({ + name: "type-operator-spacing", + meta: { + docs: { + description: ``, + category: "Stylistic Issues", + recommended: "error", + }, + messages: { + typeOperatorSpacingError: `The '|' and '&' operators must be surrounded by spaces`, + }, + schema: [], + type: "suggestion", + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.getSourceCode(); + const tokens = ["|", "&"]; + const text = sourceCode.getText(); + + const checkTypeOperatorSpacing = (node: TSESTree.TSIntersectionType | TSESTree.TSUnionType) => { + node.types.forEach(node => { + const token = sourceCode.getTokenBefore(node); + + if (!!token && token.type === AST_TOKEN_TYPES.Punctuator && tokens.indexOf(token.value) >= 0) { + const [start, end] = token.range; + + if (/\S/.test(text[start - 1]) || /\S/.test(text[end])) { + context.report({ messageId: "typeOperatorSpacingError", node: token }); + } + } + }); + }; + + return { + TSIntersectionType: checkTypeOperatorSpacing, + TSUnionType: checkTypeOperatorSpacing, + }; + }, +}); diff --git a/scripts/eslint/rules/utils.ts b/scripts/eslint/rules/utils.ts new file mode 100644 index 0000000000000..161948de34d56 --- /dev/null +++ b/scripts/eslint/rules/utils.ts @@ -0,0 +1,2 @@ +import { ESLintUtils } from "@typescript-eslint/experimental-utils"; +export const createRule = ESLintUtils.RuleCreator(() => ""); diff --git a/scripts/eslint/tests/boolean-trivia.test.ts b/scripts/eslint/tests/boolean-trivia.test.ts new file mode 100644 index 0000000000000..df8ba4e34cd39 --- /dev/null +++ b/scripts/eslint/tests/boolean-trivia.test.ts @@ -0,0 +1,83 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/boolean-trivia"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("boolean-trivia", rule, { + valid: [ + { + code: ` +const fn = (prop: boolean) => {}; +fn(/* boolean prop */ true); + `, + }, + { + code: ` +const fn = (prop: null) => {}; +fn(/* null prop */ null); + `, + }, + { + code: ` +const fn = (prop: undefined) => {}; +fn(/* undefined prop */ undefined); + `, + }, + { + code: ` +const fn = (prop: null) => {}; +fn(/*null prop*/ null); + `, + }, + { + code: ` +const fn = (prop: boolean) => {}; +fn(/* comment */ + false +); + `, + }, + { + code: ` +const fn = (prop: boolean) => {}; +fn.apply(null, true); + `, + }, + ], + + invalid: [ + { + code: ` +const fn = (prop: null) => {}; +fn(null); + `, + errors: [{ messageId: "booleanTriviaArgumentError" }], + }, + { + code: ` +const fn = (prop: boolean) => {}; +fn(false); + `, + errors: [{ messageId: "booleanTriviaArgumentError" }], + }, + { + code: ` +const fn = (prop: boolean) => {}; +fn(/* boolean arg */false); + `, + errors: [{ messageId: "booleanTriviaArgumentSpaceError" }], + }, + { + code: ` +const fn = (prop: boolean) => {}; +fn(/* first comment */ /* second comment */ false); + `, + errors: [{ messageId: "booleanTriviaArgumentError" }], + }, + ], +}); diff --git a/scripts/eslint/tests/debug-assert.test.ts b/scripts/eslint/tests/debug-assert.test.ts new file mode 100644 index 0000000000000..d0400507dc207 --- /dev/null +++ b/scripts/eslint/tests/debug-assert.test.ts @@ -0,0 +1,50 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/debug-assert"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("debug-assert", rule, { + valid: [ + { + code: `Debug.assert(true)`, + }, + { + code: `Debug.assert(true, 'error message')`, + }, + { + code: `Debug.assert(true, 'error message 1', 'error message 2')`, + }, + { + code: `Debug.assert(true, 'error message 1', () => {})`, + }, + { + code: "Debug.assert(true, `error message 1`, () => {})", + }, + { + code: `Debug.assert(true, "error message 1", () => {})`, + }, + ], + + invalid: [ + { + code: `Debug.assert(true, 1)`, + errors: [{ messageId: "secondArgumentDebugAssertError" }], + }, + { + code: `Debug.assert(true, 'error message', 1)`, + errors: [{ messageId: "thirdArgumentDebugAssertError" }], + }, + { + code: `Debug.assert(true, null, 1)`, + errors: [ + { messageId: "secondArgumentDebugAssertError" }, + { messageId: "thirdArgumentDebugAssertError" }, + ], + } + ], +}); diff --git a/scripts/eslint/tests/no-double-space.test.ts b/scripts/eslint/tests/no-double-space.test.ts new file mode 100644 index 0000000000000..b28154675b071 --- /dev/null +++ b/scripts/eslint/tests/no-double-space.test.ts @@ -0,0 +1,154 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/no-double-space"); + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, +}); + +ruleTester.run("no-double-space", rule, { + valid: [ + { + code: `const a = {};`, + }, + { + code: `function fn() {}`, + }, + { + code: `const a = " ";`, + }, + { + code: `// ^ ^`, + }, + { + code: `class Cl {}`, + }, + { + code: `// comment `, + }, + { + code: `/* comment */`, + }, + { + code: `" string ";`, + }, + { + code: `/ regexp /g;`, + }, + { + code: `const rgx = / regexp /g;`, + }, + { + code: "const str = ` string template`;", + }, + { + code: ` // comment`, + }, + { + code: ` /* comment */`, + }, + { + code: `// `, + }, + { + code: ` +const a = + 1; + `, + }, + { + code: ` +/** + * comment + */ + `, + }, + { + code: ` +// comment +// - comment +// - comment + `, + }, + { + code: ` +interface Props { + prop: string[]; // comment prop + propB: string[]; // comment propB +} + `, + }, + { + code: ` +/** + * Returns a JSON-encoded value of the type: string[] + * + * @param exclude A JSON encoded string[] containing the paths to exclude + * when enumerating the directory. + */ + `, + }, + { + code: ` +const obj = { + content: "function f() { 1; }", +}; + `, + }, + ], + + invalid: [ + { + code: `const a = {};`, + errors: [ + { messageId: "noDoubleSpaceError", line: 1, column: 6 }, + ], + }, + { + code: `function fn() {}`, + errors: [ + { messageId: "noDoubleSpaceError", line: 1, column: 9 }, + ], + }, + { + code: `class Cl {}`, + errors: [{ messageId: "noDoubleSpaceError", line: 1, column: 6 }], + }, + { + code: "const str = ` string template`;", + errors: [ + { messageId: "noDoubleSpaceError", line: 1, column: 12 }, + ], + }, + { + code: `/** comment */`, + errors: [ + { messageId: "noDoubleSpaceError", line: 1, column: 12 }, + ], + }, + { + code: `/** comment with many spaces */`, + errors: [ + { messageId: "noDoubleSpaceError", line: 1, column: 12 }, + ], + }, + { + code: `// comment with many spaces`, + errors: [ + { messageId: "noDoubleSpaceError", line: 1, column: 11 }, + ], + }, + { + code: ` +const a = 1; +const b = 2; +const c = 3; + `, + errors: [ + { messageId: "noDoubleSpaceError", line: 4, column: 10 }, + ], + }, + ], +}); diff --git a/scripts/eslint/tests/no-in-operator.test.ts b/scripts/eslint/tests/no-in-operator.test.ts new file mode 100644 index 0000000000000..aef915670fec6 --- /dev/null +++ b/scripts/eslint/tests/no-in-operator.test.ts @@ -0,0 +1,30 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/no-in-operator"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("no-in-operator", rule, { + valid: [ + { + code: ` +const prop = {}; +prop.hasProperty('a'); + `, + }, + ], + + invalid: [ + { + code: ` +const prop = {}; +prop in 'a'; + `, + errors: [{ messageId: "noInOperatorError" }], + }, + ], +}); diff --git a/scripts/eslint/tests/no-keywords.test.ts b/scripts/eslint/tests/no-keywords.test.ts new file mode 100644 index 0000000000000..18c348720599c --- /dev/null +++ b/scripts/eslint/tests/no-keywords.test.ts @@ -0,0 +1,158 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/no-keywords"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("no-keywords", rule, { + valid: [ + { + code: `const a = {};`, + }, + { + code: `function a() {};`, + }, + { + code: `const x = function string() {};`, + }, + { + code: `const y = function () {};`, + }, + { + code: `const y = () => {};`, + }, + { + code: ` +class A { + b = () => {} + a() {} + number() {} +} + `, + }, + { + code: ` +interface A { + b(): void; +} + `, + }, + { + code: ` +const obj = { + a: null, + b() {}, + c: function (c: number) {}, + string: function d (d: string) {}, + e: () => {}, +}; + `, + }, + { + code: `() => undefined`, + }, + { + code: `let a = 2;`, + }, + { + code: `let b: any;`, + }, + { + code: `function foo(a: any) { }`, + }, + { + code: `let [a] = [5];`, + }, + { + code: `const { a } = { a: 5 };`, + }, + { + code: ` +interface Foo { + number: string; +} + `, + } + ], + + invalid: [ + { + code: `const number = 1;`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `function x(number: number) {};`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `const y = function (number: number) {};`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `const y = (number: number) => {};`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: ` +class A { + b = function (any: any) {}; + a(number: number) {} +} + `, + errors: [ + { messageId: "noKeywordsError" }, + { messageId: "noKeywordsError" }, + ], + }, + { + code: ` +interface A { + a(number: number): void; + b: (any: any) => void; +} + `, + errors: [ + { messageId: "noKeywordsError" }, + { messageId: "noKeywordsError" }, + ], + }, + { + code: `let undefined = 8;`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `let boolean: boolean;`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `function foo(any: any) { }`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `let [number] = [3];`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `let { String } = { String: 1 };`, + errors: [{ messageId: "noKeywordsError" }], + }, + { + code: `let [number, string] = [3, ''];`, + errors: [ + { messageId: "noKeywordsError" }, + { messageId: "noKeywordsError" } + ], + }, + { + code: `let { String, Boolean } = { String: 1, Boolean: false };`, + errors: [ + { messageId: "noKeywordsError" }, + { messageId: "noKeywordsError" } + ], + }, + ], +}); diff --git a/scripts/eslint/tests/no-type-assertion-whitespace.test.ts b/scripts/eslint/tests/no-type-assertion-whitespace.test.ts new file mode 100644 index 0000000000000..4cc68fd3a06d9 --- /dev/null +++ b/scripts/eslint/tests/no-type-assertion-whitespace.test.ts @@ -0,0 +1,39 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/no-type-assertion-whitespace"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("no-type-assertion-whitespace", rule, { + valid: [ + { + code: `const a = 1`, + }, + { + code: `const s = (new Symbol(1, 'name'));`, + }, + ], + + invalid: [ + { + code: `const a = 1`, + errors: [{ messageId: "noTypeAssertionWhitespace", column: 19, line: 1 }], + }, + { + code: `const a = 1`, + errors: [{ messageId: "noTypeAssertionWhitespace", column: 19, line: 1 }], + }, + { + code: `const a = 1`, + errors: [{ messageId: "noTypeAssertionWhitespace", column: 19, line: 1 }], + }, + { + code: `const s = (new Symbol(1, 'name'));`, + errors: [{ messageId: "noTypeAssertionWhitespace", column: 17, line: 1 }], + }, + ], +}); diff --git a/scripts/eslint/tests/object-literal-surrounding-space.test.ts b/scripts/eslint/tests/object-literal-surrounding-space.test.ts new file mode 100644 index 0000000000000..2e94a2b61f3b6 --- /dev/null +++ b/scripts/eslint/tests/object-literal-surrounding-space.test.ts @@ -0,0 +1,49 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/object-literal-surrounding-space"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("object-literal-surrounding-space", rule, { + valid: [ + { + code: `const prop = {}`, + }, + { + code: `const prop = { }`, + }, + { + code: `const prop = { x: 1 }`, + }, + ], + + invalid: [ + { + code: `const prop = {x: 1}`, + errors: [ + { messageId: "leadingStringError" }, + { messageId: "trailingStringError" } + ], + }, + { + code: `const prop = { x: 1 }`, + errors: [{ messageId: "leadingExcessStringError" }], + }, + { + code: `const prop = { x: 1 }`, + errors: [{ messageId: "trailingExcessStringError" }], + }, + { + code: `const prop = { x: 1}`, + errors: [{ messageId: "trailingStringError" }], + }, + { + code: `const prop = {x: 1 }`, + errors: [{ messageId: "leadingStringError" }], + }, + ], +}); diff --git a/scripts/eslint/tests/only-arrow-functions.test.ts b/scripts/eslint/tests/only-arrow-functions.test.ts new file mode 100644 index 0000000000000..0909e5b58685d --- /dev/null +++ b/scripts/eslint/tests/only-arrow-functions.test.ts @@ -0,0 +1,127 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/only-arrow-functions"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("only-arrow-functions", rule, { + valid: [ + { + code: `const a = () => {};`, + }, + { + code: `((func) => func())(() => {});`, + }, + { + code: `function* generator() {}`, + }, + { + code: `let generator = function*() {}`, + }, + { + code: `function hasThisParameter(this) {}`, + }, + { + code: `let hasThisParameter = function(this) {}`, + }, + { + code: `let usesThis = function() { this; }`, + }, + { + code: `let usesThis2 = function(foo = this) {}`, + }, + { + code: ` +let fn = function fn() {}; +function z() {}; + `, + options: [{ allowNamedFunctions: true }], + }, + { + code: ` +function fn() {}; +let generator = function*() {} +function hasThisParameter(this) {} +let hasThisParameter = function(this) {} + `, + options: [{ allowDeclarations: true }], + }, + { + code: ` +class A { + test() {} +} + `, + }, + { + code: ` +const obj = { + test() {} +} + `, + }, + ], + + invalid: [ + { + code: `function foo(a: any): any {}`, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: `let b = function () {};`, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: `function c() {}`, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: `((func) => func())(function e(): void {});`, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: ` +let notUsesThis = function() { + function f() { this; } +} + `, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: ` +let notUsesThis2 = function() { + return class { method() { this; } } +} + `, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: `export function exported() {}`, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: `async function asyncFunction() {}`, + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + { + code: ` +let b = function () {}; +((func) => func())(function e(): void {}); + `, + options: [{ allowDeclarations: true }], + errors: [ + { messageId: "onlyArrowFunctionsError" }, + { messageId: "onlyArrowFunctionsError" }, + ], + }, + { + code: `const x = function() {}`, + options: [{ allowNamedFunctions: true }], + errors: [{ messageId: "onlyArrowFunctionsError" }], + }, + ], +}); diff --git a/scripts/eslint/tests/simple-indent.test.ts b/scripts/eslint/tests/simple-indent.test.ts new file mode 100644 index 0000000000000..161755c366bad --- /dev/null +++ b/scripts/eslint/tests/simple-indent.test.ts @@ -0,0 +1,333 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/simple-indent"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("simple-indent", rule, { + valid: [ + { + code: ` +/** + * Comment + */ + ` + }, + { + code: ` +module TestModule { + var func = () => { + console.warn("hi"); + }; +} + `, + }, + { + code: ` +class TestClass { + private variable; + + testFunction() { + this.variable = 3; + } +} + `, + }, + { + code: ` +var obj = { + a: 1, + b: 2, + c: 3 +}; + `, + }, + { + code: ` +export enum TestEnum { + VALUE1, + VALUE2 +} + `, + }, + { + code: ` +switch (integerValue) { + case 1: + console.warn("1"); + break; + default: + console.warn("default"); + break; +} + `, + }, + { + code: ` +function loops() { + for (var i = 0; i < 1; ++i) { + console.warn(i); + } + + while (i < 1) { + console.warn(i); + } + + do { + console.warn(i); + } while (i < 1); + + if (i < 1) { + console.warn(i); + } else { + console.warn(i + 1); + } +} + `, + }, + ], + + invalid: [ + { + code: ` +module TestModule { +\tvar testVariable = 123; +} + `, + output: ` +module TestModule { + var testVariable = 123; +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ], + }, + { + code: ` +function a() { +\t\tvar test = 123; +} + `, + output: ` +function a() { + var test = 123; +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 3 }, + ], + }, + { + code: ` +class TestClass { +\tprivate variable; + +\ttestFunction() { +\t\tthis.variable = 3; +\t} +} + `, + output: ` +class TestClass { + private variable; + + testFunction() { + this.variable = 3; + } +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + { messageId: "simpleIndentError", line: 5, column: 2 }, + { messageId: "simpleIndentError", line: 6, column: 3 }, + { messageId: "simpleIndentError", line: 7, column: 2 }, + ], + }, + { + code: ` +var obj = { +\ta: 1, +\tb: 2, +\tc: 3 +}; + `, + output: ` +var obj = { + a: 1, + b: 2, + c: 3 +}; + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + { messageId: "simpleIndentError", line: 4, column: 2 }, + { messageId: "simpleIndentError", line: 5, column: 2 }, + ] + }, + { + code: ` +enum TestEnum { +\tVALUE1, + VALUE2 +} + `, + output: ` +enum TestEnum { + VALUE1, + VALUE2 +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ], + }, + { + code: ` +switch (integerValue) { +\tcase 0: +\t\tconsole.warn("1"); +\t\tbreak; + case 1: + console.warn("1"); + break; +\tdefault: +\t\tconsole.log("2"); +\t\tbreak; +} + `, + output: ` +switch (integerValue) { + case 0: + console.warn("1"); + break; + case 1: + console.warn("1"); + break; + default: + console.log("2"); + break; +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + { messageId: "simpleIndentError", line: 4, column: 3 }, + { messageId: "simpleIndentError", line: 5, column: 3 }, + { messageId: "simpleIndentError", line: 9, column: 2 }, + { messageId: "simpleIndentError", line: 10, column: 3 }, + { messageId: "simpleIndentError", line: 11, column: 3 }, + ] + }, + { + code: ` +for (var i = 0; i < 1; ++i) { +\tconsole.warn("123"); +} + `, + output: ` +for (var i = 0; i < 1; ++i) { + console.warn("123"); +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ], + }, + { + code: ` +while (i < 1) { +\tconsole.warn("123"); +} + `, + output: ` +while (i < 1) { + console.warn("123"); +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ] + }, + { + code: ` +do { +\tconsole.warn("123"); +} while (i < 1); + `, + output: ` +do { + console.warn("123"); +} while (i < 1); + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ] + }, + { + code: ` +if (i < 1) { +\tconsole.warn("123"); +} + `, + output: ` +if (i < 1) { + console.warn("123"); +} + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ] + }, + { + code: ` +var arr = [ +\t1, + 2 +]; + `, + output: ` +var arr = [ + 1, + 2 +]; + `, + errors: [ + { messageId: "simpleIndentError", line: 3, column: 2 }, + ] + }, + { + code: ` +var arr2 = [ + { +\t\ta: 1, + b: 2 + }, + { + a: 3, +\t\tb: 4 + } +]; + `, + output: ` +var arr2 = [ + { + a: 1, + b: 2 + }, + { + a: 3, + b: 4 + } +]; + `, + errors: [ + { messageId: "simpleIndentError", line: 4, column: 3 }, + { messageId: "simpleIndentError", line: 9, column: 3 }, + ] + } + ], +}); diff --git a/scripts/eslint/tests/support/RuleTester.ts b/scripts/eslint/tests/support/RuleTester.ts new file mode 100644 index 0000000000000..adbae651d2dfa --- /dev/null +++ b/scripts/eslint/tests/support/RuleTester.ts @@ -0,0 +1,6 @@ +import * as path from "path"; +import { TSESLint } from "@typescript-eslint/experimental-utils"; + +export const ROOT_DIR = path.join(process.cwd(), "scripts", "eslint", "tests", "fixtures"); +export const FILENAME = path.join(ROOT_DIR, "file.ts"); +export const RuleTester = TSESLint.RuleTester; diff --git a/scripts/eslint/tests/type-operator-spacing.test.ts b/scripts/eslint/tests/type-operator-spacing.test.ts new file mode 100644 index 0000000000000..7654dfc9c53ec --- /dev/null +++ b/scripts/eslint/tests/type-operator-spacing.test.ts @@ -0,0 +1,45 @@ +import { RuleTester } from "./support/RuleTester"; +import rule = require("../rules/type-operator-spacing"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("type-operator-spacing", rule, { + valid: [ + { + code: `type T = string | number`, + }, + { + code: `type T = string & number`, + }, + { + code: `function fn(): string | number {}`, + }, + { + code: `function fn(): string & number {}`, + }, + ], + + invalid: [ + { + code: `type T = string|number`, + errors: [{ messageId: "typeOperatorSpacingError" }], + }, + { + code: `type T = string&number`, + errors: [{ messageId: "typeOperatorSpacingError" }], + }, + { + code: `function fn(): string|number {}`, + errors: [{ messageId: "typeOperatorSpacingError" }], + }, + { + code: `function fn(): string&number {}`, + errors: [{ messageId: "typeOperatorSpacingError" }], + }, + ], +}); diff --git a/scripts/eslint/tsconfig.json b/scripts/eslint/tsconfig.json new file mode 100644 index 0000000000000..254e074b11bef --- /dev/null +++ b/scripts/eslint/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "suppressImplicitAnyIndexErrors": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "esModuleInterop": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noImplicitAny": true, + "skipLibCheck": true, + "declaration": false, + "noResolve": false, + "strict": true, + "module": "commonjs", + "target": "es6", + "outDir": "./built", + "lib": ["es2015", "es2016"] + }, + + "include": [ + "rules", + "tests" + ] +} diff --git a/scripts/failed-tests.d.ts b/scripts/failed-tests.d.ts index e82c0b178e8be..5218a75910e34 100644 --- a/scripts/failed-tests.d.ts +++ b/scripts/failed-tests.d.ts @@ -8,7 +8,7 @@ declare class FailedTestsReporter extends Mocha.reporters.Base { reporterOptions: FailedTestsReporter.ReporterOptions; reporter?: Mocha.reporters.Base; constructor(runner: Mocha.Runner, options?: { reporterOptions?: FailedTestsReporter.ReporterOptions }); - static writeFailures(file: string, passes: ReadonlyArray, failures: ReadonlyArray, keepFailed: boolean, done: (err?: NodeJS.ErrnoException) => void): void; + static writeFailures(file: string, passes: readonly Mocha.Test[], failures: readonly Mocha.Test[], keepFailed: boolean, done: (err?: NodeJS.ErrnoException) => void): void; done(failures: number, fn?: (failures: number) => void): void; } @@ -19,4 +19,4 @@ declare namespace FailedTestsReporter { reporter?: string | Mocha.ReporterConstructor; reporterOptions?: any; } -} \ No newline at end of file +} diff --git a/scripts/generateLocalizedDiagnosticMessages.ts b/scripts/generateLocalizedDiagnosticMessages.ts index 98bae170baa39..abfe6a3cff93e 100644 --- a/scripts/generateLocalizedDiagnosticMessages.ts +++ b/scripts/generateLocalizedDiagnosticMessages.ts @@ -1,177 +1,177 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as xml2js from "xml2js"; - -function main(): void { - const args = process.argv.slice(2); - if (args.length !== 3) { - console.log("Usage:"); - console.log("\tnode generateLocalizedDiagnosticMessages.js "); - return; - } - - const inputPath = args[0]; - const outputPath = args[1]; - const diagnosticsMapFilePath = args[2]; - - // generate the lcg file for enu - generateLCGFile(); - - // generate other langs - fs.readdir(inputPath, (err, files) => { - handleError(err); - files.forEach(visitDirectory); - }); - - return; - - function visitDirectory(name: string) { - const inputFilePath = path.join(inputPath, name, "diagnosticMessages", "diagnosticMessages.generated.json.lcl"); - - fs.readFile(inputFilePath, (err, data) => { - handleError(err); - xml2js.parseString(data.toString(), (err, result) => { - handleError(err); - if (!result || !result.LCX || !result.LCX.$ || !result.LCX.$.TgtCul) { - console.error("Unexpected XML file structure. Expected to find result.LCX.$.TgtCul."); - process.exit(1); - } - const outputDirectoryName = getPreferredLocaleName(result.LCX.$.TgtCul).toLowerCase(); - if (!outputDirectoryName) { - console.error(`Invalid output locale name for '${result.LCX.$.TgtCul}'.`); - process.exit(1); - } - writeFile(path.join(outputPath, outputDirectoryName, "diagnosticMessages.generated.json"), xmlObjectToString(result)); - }); - }); - } - - /** - * A locale name is based on the language tagging conventions of RFC 4646 (Windows Vista - * and later), and is represented by LOCALE_SNAME. - * Generally, the pattern - is used. Here, language is a lowercase ISO 639 - * language code. The codes from ISO 639-1 are used when available. Otherwise, codes from - * ISO 639-2/T are used. REGION specifies an uppercase ISO 3166-1 country/region identifier. - * For example, the locale name for English (United States) is "en-US" and the locale name - * for Divehi (Maldives) is "dv-MV". - * - * If the locale is a neutral locale (no region), the LOCALE_SNAME value follows the - * pattern . If it is a neutral locale for which the script is significant, the - * pattern is -