Skip to content

Commit adb5ac9

Browse files
jhanschooHyphnKnight
authored andcommitted
Support yaml configuration (palantir#1598) (palantir#3433)
1 parent 1fd8ae2 commit adb5ac9

File tree

13 files changed

+146
-37
lines changed

13 files changed

+146
-37
lines changed

docs/usage/configuration/index.md

+35-4
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ title: Configuring TSLint
44
permalink: /usage/configuration/
55
---
66

7-
### tslint.json
7+
### TSLint Configuration
88

9-
When using [the CLI][0] or many [third-party tools][1], a file named `tslint.json` is used to
10-
configure which rules get run and each of their options. This configuration file may be comp
9+
When using [the CLI][0] or many [third-party tools][1], a file named `tslint.json` or `tslint.yaml` is used to
10+
configure which rules get run and each of their options.
1111

12-
`tslint.json` files can have the following fields specified:
12+
`tslint.json` or `tslint.yaml` files can have the following fields specified:
1313

1414
* `extends?: string | string[]`:
1515
The name of a built-in configuration preset (see built-in presets below), or a path or
@@ -71,6 +71,37 @@ An example `tslint.json` file might look like this:
7171
}
7272
```
7373

74+
The corresponding YAML file looks like this:
75+
76+
```yaml
77+
---
78+
extends: "tslint:recommended"
79+
rulesDirectory:
80+
- path/to/custom/rules/directory/
81+
- another/path/
82+
rules:
83+
max-line-length:
84+
options: [120]
85+
new-parens: true
86+
no-arg: true
87+
no-bitwise: true
88+
no-conditional-assignment: true
89+
no-consecutive-blank-lines: false
90+
no-console:
91+
severity: warning
92+
options:
93+
- debug
94+
- info
95+
- log
96+
- time
97+
- timeEnd
98+
- trace
99+
jsRules:
100+
max-line-length:
101+
options: [120]
102+
...
103+
```
104+
74105
### Rule severity
75106

76107
The severity level of each rule can can be configured to `default`, `error`, `warning`/`warn`, or `off`/`none`. If no severity level is specified, `default` is used. The `defaultSeverity` top-level option replaces the severity level for each rule that uses severity level `default` in the current file. Valid values for `defaultSeverity` include `error`, `warning`/`warn`, and `off`/`none`.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"commander": "^2.9.0",
4444
"diff": "^3.2.0",
4545
"glob": "^7.1.1",
46+
"js-yaml": "^3.7.0",
4647
"minimatch": "^3.0.4",
4748
"resolve": "^1.3.2",
4849
"semver": "^5.3.0",
@@ -67,7 +68,6 @@
6768
"@types/semver": "^5.3.30",
6869
"chai": "^3.5.0",
6970
"github": "^8.2.1",
70-
"js-yaml": "^3.7.0",
7171
"json-stringify-pretty-compact": "^1.0.3",
7272
"mocha": "^3.2.0",
7373
"npm-run-all": "^4.0.2",

src/configuration.ts

+44-25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import * as fs from "fs";
19+
import * as yaml from "js-yaml";
1920
import * as path from "path";
2021
import * as resolve from "resolve";
2122
import { FatalError, showWarningOnce } from "./error";
@@ -63,7 +64,10 @@ export interface IConfigurationLoadResult {
6364
results?: IConfigurationFile;
6465
}
6566

66-
export const CONFIG_FILENAME = "tslint.json";
67+
// Note: eslint prefers yaml over json, while tslint prefers json over yaml
68+
// for backward-compatibility.
69+
export const JSON_CONFIG_FILENAME = "tslint.json";
70+
export const CONFIG_FILENAMES = [JSON_CONFIG_FILENAME, "tslint.yaml", "tslint.yml"];
6771

6872
export const DEFAULT_CONFIG: IConfigurationFile = {
6973
defaultSeverity: "error",
@@ -111,7 +115,7 @@ export function findConfiguration(configFile: string | null, inputFilePath?: str
111115
* the location of the config file is not known and you want to search for one.
112116
* @param inputFilePath A path to the current file being linted. This is the starting location
113117
* of the search for a configuration.
114-
* @returns An absolute path to a tslint.json file
118+
* @returns An absolute path to a tslint.json or tslint.yml or tslint.yaml file
115119
* or undefined if neither can be found.
116120
*/
117121
export function findConfigurationPath(suppliedConfigFilePath: string | null, inputFilePath: string): string | undefined;
@@ -140,17 +144,19 @@ export function findConfigurationPath(suppliedConfigFilePath: string | null, inp
140144
}
141145

142146
// search for tslint.json from input file location
143-
let configFilePath = findup(CONFIG_FILENAME, path.resolve(inputFilePath!));
147+
let configFilePath = findup(CONFIG_FILENAMES, path.resolve(inputFilePath!));
144148
if (configFilePath !== undefined) {
145149
return configFilePath;
146150
}
147151

148152
// search for tslint.json in home directory
149153
const homeDir = getHomeDir();
150154
if (homeDir != undefined) {
151-
configFilePath = path.join(homeDir, CONFIG_FILENAME);
152-
if (fs.existsSync(configFilePath)) {
153-
return path.resolve(configFilePath);
155+
for (const configFilename of CONFIG_FILENAMES) {
156+
configFilePath = path.join(homeDir, configFilename);
157+
if (fs.existsSync(configFilePath)) {
158+
return path.resolve(configFilePath);
159+
}
154160
}
155161
}
156162
// no path could be found
@@ -159,10 +165,11 @@ export function findConfigurationPath(suppliedConfigFilePath: string | null, inp
159165
}
160166

161167
/**
162-
* Find a file by name in a directory or any ancestory directory.
168+
* Find a file by names in a directory or any ancestor directory.
169+
* Will try each filename in filenames before recursing to a parent directory.
163170
* This is case-insensitive, so it can find 'TsLiNt.JsOn' when searching for 'tslint.json'.
164171
*/
165-
function findup(filename: string, directory: string): string | undefined {
172+
function findup(filenames: string[], directory: string): string | undefined {
166173
while (true) {
167174
const res = findFile(directory);
168175
if (res !== undefined) {
@@ -177,18 +184,21 @@ function findup(filename: string, directory: string): string | undefined {
177184
}
178185

179186
function findFile(cwd: string): string | undefined {
180-
if (fs.existsSync(path.join(cwd, filename))) {
181-
return filename;
182-
}
183-
184-
// TODO: remove in v6.0.0
185-
// Try reading in the entire directory and looking for a file with different casing.
186-
const filenameLower = filename.toLowerCase();
187-
const result = fs.readdirSync(cwd).find((entry) => entry.toLowerCase() === filenameLower);
188-
if (result !== undefined) {
189-
showWarningOnce(`Using mixed case tslint.json is deprecated. Found: ${path.join(cwd, result)}`);
187+
const dirFiles = fs.readdirSync(cwd);
188+
for (const filename of filenames) {
189+
const index = dirFiles.indexOf(filename);
190+
if (index > -1) {
191+
return filename;
192+
}
193+
// TODO: remove in v6.0.0
194+
// Try reading in the entire directory and looking for a file with different casing.
195+
const result = dirFiles.find((entry) => entry.toLowerCase() === filename);
196+
if (result !== undefined) {
197+
showWarningOnce(`Using mixed case ${filename} is deprecated. Found: ${path.join(cwd, result)}`);
198+
return result;
199+
}
190200
}
191-
return result;
201+
return undefined;
192202
}
193203
}
194204

@@ -207,13 +217,23 @@ export function loadConfigurationFromPath(configFilePath?: string, originalFileP
207217
return DEFAULT_CONFIG;
208218
} else {
209219
const resolvedConfigFilePath = resolveConfigurationPath(configFilePath);
220+
const resolvedConfigFileExt = path.extname(resolvedConfigFilePath);
210221
let rawConfigFile: RawConfigFile;
211-
if (path.extname(resolvedConfigFilePath) === ".json") {
212-
const fileContent = stripComments(fs.readFileSync(resolvedConfigFilePath)
213-
.toString()
214-
.replace(/^\uFEFF/, ""));
222+
if (/\.(json|ya?ml)/.test(resolvedConfigFileExt)) {
223+
const fileContent = fs.readFileSync(resolvedConfigFilePath, "utf8").replace(/^\uFEFF/, "");
215224
try {
216-
rawConfigFile = JSON.parse(fileContent) as RawConfigFile;
225+
if (resolvedConfigFileExt === ".json") {
226+
rawConfigFile = JSON.parse(stripComments(fileContent)) as RawConfigFile;
227+
} else {
228+
// choose this branch only if /\.ya?ml/.test(resolvedConfigFileExt) === true
229+
rawConfigFile = yaml.safeLoad(fileContent, {
230+
// Note: yaml.LoadOptions expects a schema value of type "any",
231+
// but this trips up the no-unsafe-any rule.
232+
// tslint:disable-next-line:no-unsafe-any
233+
schema: yaml.JSON_SCHEMA,
234+
strict: true,
235+
}) as RawConfigFile;
236+
}
217237
} catch (e) {
218238
const error = e as Error;
219239
// include the configuration file being parsed in the error since it may differ from the directly referenced config
@@ -286,7 +306,6 @@ export function extendConfigurationFile(targetConfig: IConfigurationFile,
286306
}
287307
}
288308
}
289-
290309
}
291310
}
292311

src/runner.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ import * as path from "path";
2424
import * as ts from "typescript";
2525

2626
import {
27-
CONFIG_FILENAME,
2827
DEFAULT_CONFIG,
2928
findConfiguration,
3029
IConfigurationFile,
30+
JSON_CONFIG_FILENAME,
3131
} from "./configuration";
3232
import { FatalError } from "./error";
3333
import { LintResult } from "./index";
@@ -131,11 +131,11 @@ export async function run(options: Options, logger: Logger): Promise<Status> {
131131

132132
async function runWorker(options: Options, logger: Logger): Promise<Status> {
133133
if (options.init) {
134-
if (fs.existsSync(CONFIG_FILENAME)) {
135-
throw new FatalError(`Cannot generate ${CONFIG_FILENAME}: file already exists`);
134+
if (fs.existsSync(JSON_CONFIG_FILENAME)) {
135+
throw new FatalError(`Cannot generate ${JSON_CONFIG_FILENAME}: file already exists`);
136136
}
137137

138-
fs.writeFileSync(CONFIG_FILENAME, JSON.stringify(DEFAULT_CONFIG, undefined, " "));
138+
fs.writeFileSync(JSON_CONFIG_FILENAME, JSON.stringify(DEFAULT_CONFIG, undefined, " "));
139139
return Status.Ok;
140140
}
141141

test/config/tslint-invalid.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ hello I am invalid }

test/config/tslint-with-comments.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
jsRules:
3+
# rule-one: true
4+
rule-two:
5+
severity: error # after comment
6+
rule-three:
7+
- true
8+
- "#not a comment"
9+
# a noice comment
10+
rules:
11+
# rule-one: true
12+
rule-two:
13+
severity: error # after comment
14+
rule-three:
15+
- true
16+
- "#not a comment"
17+
...

test/configurationTests.ts

+21
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,16 @@ describe("Configuration", () => {
274274
path.resolve("./test/files/config-findup/tslint.json"),
275275
);
276276
});
277+
it("prefers json over yaml over yml configuration files", () => {
278+
assert.strictEqual(
279+
findConfigurationPath(null, "./test/files/config-findup/yaml-config"),
280+
path.resolve("test/files/config-findup/yaml-config/tslint.json"),
281+
);
282+
assert.strictEqual(
283+
findConfigurationPath(null, "./test/files/config-findup/yml-config"),
284+
path.resolve("test/files/config-findup/yml-config/tslint.yaml"),
285+
);
286+
});
277287
});
278288

279289
describe("loadConfigurationFromPath", () => {
@@ -421,6 +431,17 @@ describe("Configuration", () => {
421431
assert.doesNotThrow(() => loadConfigurationFromPath("./test/config/tslint-with-bom.json"));
422432
});
423433

434+
it("can load .yaml files with comments", () => {
435+
const config = loadConfigurationFromPath("./test/config/tslint-with-comments.yaml");
436+
437+
const expectedConfig = getEmptyConfig();
438+
expectedConfig.rules.set("rule-two", { ruleSeverity: "error" });
439+
expectedConfig.rules.set("rule-three", { ruleSeverity: "error", ruleArguments: ["#not a comment"] });
440+
441+
assertConfigEquals(config.rules, expectedConfig.rules);
442+
assertConfigEquals(config.jsRules, expectedConfig.rules);
443+
});
444+
424445
it("can load a built-in configuration", () => {
425446
const config = loadConfigurationFromPath("tslint:recommended");
426447
assert.strictEqual<RuleSeverity | undefined>("error", config.jsRules.get("no-eval")!.ruleSeverity);

test/executable/executableTests.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,23 @@ describe("Executable", function(this: Mocha.ISuiteCallbackContext) {
8787
});
8888
});
8989

90-
it("exits with code 1 if config file is invalid", (done) => {
90+
it("exits with code 1 if json config file is invalid", (done) => {
9191
execCli(["-c", "test/config/tslint-invalid.json", "src/test.ts"], (err, stdout, stderr) => {
9292
assert.isNotNull(err, "process should exit with error");
9393
assert.strictEqual(err.code, 1, "error code should be 1");
9494

95-
assert.include(stderr, "Failed to load", "stderr should contain notification about failing to load json");
95+
assert.include(stderr, "Failed to load", "stderr should contain notification about failing to load json config");
96+
assert.strictEqual(stdout, "", "shouldn't contain any output in stdout");
97+
done();
98+
});
99+
});
100+
101+
it("exits with code 1 if yaml config file is invalid", (done) => {
102+
execCli(["-c", "test/config/tslint-invalid.yaml", "src/test.ts"], (err, stdout, stderr) => {
103+
assert.isNotNull(err, "process should exit with error");
104+
assert.strictEqual(err.code, 1, "error code should be 1");
105+
106+
assert.include(stderr, "Failed to load", "stderr should contain notification about failing to load yaml config");
96107
assert.strictEqual(stdout, "", "shouldn't contain any output in stdout");
97108
done();
98109
});
@@ -103,7 +114,7 @@ describe("Executable", function(this: Mocha.ISuiteCallbackContext) {
103114
assert.isNotNull(err, "process should exit with error");
104115
assert.strictEqual(err.code, 1, "error code should be 1");
105116

106-
assert.include(stderr, "Failed to load", "stderr should contain notification about failing to load json");
117+
assert.include(stderr, "Failed to load", "stderr should contain notification about failing to load json config");
107118
assert.include(stderr, "tslint-invalid.json", "stderr should mention the problem file");
108119
assert.strictEqual(stdout, "", "shouldn't contain any output in stdout");
109120
done();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
...

0 commit comments

Comments
 (0)