options-manager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import path from 'path';
  2. import test from 'ava';
  3. import proxyquire from 'proxyquire';
  4. import parentConfig from './fixtures/nested/package';
  5. import childConfig from './fixtures/nested/child/package';
  6. import prettierConfig from './fixtures/prettier/package';
  7. import enginesConfig from './fixtures/engines/package';
  8. process.chdir(__dirname);
  9. const manager = proxyquire('../lib/options-manager', {
  10. 'resolve-from': (cwd, path) => `cwd/${path}`
  11. });
  12. test('normalizeOpts: makes all the opts plural and arrays', t => {
  13. const opts = manager.normalizeOpts({
  14. env: 'node',
  15. global: 'foo',
  16. ignore: 'test.js',
  17. plugin: 'my-plugin',
  18. rule: {'my-rule': 'foo'},
  19. setting: {'my-rule': 'bar'},
  20. extend: 'foo',
  21. extension: 'html'
  22. });
  23. t.deepEqual(opts, {
  24. envs: ['node'],
  25. globals: ['foo'],
  26. ignores: ['test.js'],
  27. plugins: ['my-plugin'],
  28. rules: {'my-rule': 'foo'},
  29. settings: {'my-rule': 'bar'},
  30. extends: ['foo'],
  31. extensions: ['html']
  32. });
  33. });
  34. test('normalizeOpts: falsie values stay falsie', t => {
  35. t.deepEqual(manager.normalizeOpts({}), {});
  36. });
  37. test('buildConfig: defaults', t => {
  38. const config = manager.buildConfig({});
  39. t.true(/[\\/]\.xo-cache[\\/]?$/.test(config.cacheLocation));
  40. t.is(config.useEslintrc, false);
  41. t.is(config.cache, true);
  42. t.is(config.baseConfig.extends[0], 'xo/esnext');
  43. });
  44. test('buildConfig: esnext', t => {
  45. const config = manager.buildConfig({esnext: false});
  46. t.is(config.baseConfig.extends[0], 'xo');
  47. });
  48. test('buildConfig: space: true', t => {
  49. const config = manager.buildConfig({space: true});
  50. t.deepEqual(config.rules.indent, ['error', 2, {SwitchCase: 1}]);
  51. });
  52. test('buildConfig: space: 4', t => {
  53. const config = manager.buildConfig({space: 4});
  54. t.deepEqual(config.rules.indent, ['error', 4, {SwitchCase: 1}]);
  55. });
  56. test('buildConfig: semicolon', t => {
  57. const config = manager.buildConfig({semicolon: false});
  58. t.deepEqual(config.rules, {
  59. semi: ['error', 'never'],
  60. 'semi-spacing': ['error', {
  61. before: false,
  62. after: true
  63. }]
  64. });
  65. });
  66. test('buildConfig: prettier: true', t => {
  67. const config = manager.buildConfig({prettier: true, extends: ['xo-react']});
  68. t.deepEqual(config.plugins, ['prettier']);
  69. // Sets the `semi`, `useTabs` and `tabWidth` options in `prettier/prettier` based on the XO `space` and `semicolon` options
  70. // Sets `singleQuote`, `trailingComma`, `bracketSpacing` and `jsxBracketSameLine` with XO defaults
  71. t.deepEqual(config.rules['prettier/prettier'], ['error', {
  72. useTabs: true,
  73. bracketSpacing: false,
  74. jsxBracketSameLine: false,
  75. semi: true,
  76. singleQuote: true,
  77. tabWidth: 2,
  78. trailingComma: 'none'
  79. }]);
  80. // eslint-prettier-config must always be last
  81. t.deepEqual(config.baseConfig.extends.slice(-1), ['prettier']);
  82. // Indent rule is not enabled
  83. t.is(config.rules.indent, undefined);
  84. // Semi rule is not enabled
  85. t.is(config.rules.semi, undefined);
  86. // Semi-spacing is not enabled
  87. t.is(config.rules['semi-spacing'], undefined);
  88. });
  89. test('buildConfig: prettier: true, semicolon: false', t => {
  90. const config = manager.buildConfig({prettier: true, semicolon: false});
  91. // Sets the `semi` options in `prettier/prettier` based on the XO `semicolon` option
  92. t.deepEqual(config.rules['prettier/prettier'], ['error', {
  93. useTabs: true,
  94. bracketSpacing: false,
  95. jsxBracketSameLine: false,
  96. semi: false,
  97. singleQuote: true,
  98. tabWidth: 2,
  99. trailingComma: 'none'
  100. }]);
  101. // Indent rule is not enabled
  102. t.is(config.rules.indent, undefined);
  103. // Semi rule is not enabled
  104. t.is(config.rules.semi, undefined);
  105. // Semi-spacing is not enabled
  106. t.is(config.rules['semi-spacing'], undefined);
  107. });
  108. test('buildConfig: prettier: true, space: 4', t => {
  109. const config = manager.buildConfig({prettier: true, space: 4});
  110. // Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options
  111. t.deepEqual(config.rules['prettier/prettier'], ['error', {
  112. useTabs: false,
  113. bracketSpacing: false,
  114. jsxBracketSameLine: false,
  115. semi: true,
  116. singleQuote: true,
  117. tabWidth: 4,
  118. trailingComma: 'none'
  119. }]);
  120. // Indent rule is not enabled
  121. t.is(config.rules.indent, undefined);
  122. // Semi rule is not enabled
  123. t.is(config.rules.semi, undefined);
  124. // Semi-spacing is not enabled
  125. t.is(config.rules['semi-spacing'], undefined);
  126. });
  127. test('buildConfig: prettier: true, esnext: false', t => {
  128. const config = manager.buildConfig({prettier: true, esnext: false});
  129. // Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options
  130. t.deepEqual(config.rules['prettier/prettier'], ['error', {
  131. useTabs: true,
  132. bracketSpacing: false,
  133. jsxBracketSameLine: false,
  134. semi: true,
  135. singleQuote: true,
  136. tabWidth: 2,
  137. trailingComma: 'none'
  138. }]);
  139. // Indent rule is not enabled
  140. t.is(config.rules.indent, undefined);
  141. // Semi rule is not enabled
  142. t.is(config.rules.semi, undefined);
  143. // Semi-spacing is not enabled
  144. t.is(config.rules['semi-spacing'], undefined);
  145. });
  146. test('buildConfig: prettier: true, space: true', t => {
  147. const config = manager.buildConfig({prettier: true, space: true});
  148. // Sets `useTabs` and `tabWidth` options in `prettier/prettier` rule based on the XO `space` options
  149. t.deepEqual(config.rules['prettier/prettier'], ['error', {
  150. useTabs: false,
  151. bracketSpacing: false,
  152. jsxBracketSameLine: false,
  153. semi: true,
  154. singleQuote: true,
  155. tabWidth: 2,
  156. trailingComma: 'none'
  157. }]);
  158. // Indent rule is not enabled
  159. t.is(config.rules.indent, undefined);
  160. // Semi rule is not enabled
  161. t.is(config.rules.semi, undefined);
  162. // Semi-spacing is not enabled
  163. t.is(config.rules['semi-spacing'], undefined);
  164. });
  165. test('buildConfig: merge with prettier config', t => {
  166. const cwd = path.resolve('fixtures', 'prettier');
  167. const config = manager.buildConfig({cwd, prettier: true});
  168. // Sets the `semi` options in `prettier/prettier` based on the XO `semicolon` option
  169. t.deepEqual(config.rules['prettier/prettier'], ['error', prettierConfig.prettier]);
  170. // Indent rule is not enabled
  171. t.is(config.rules.indent, undefined);
  172. // Semi rule is not enabled
  173. t.is(config.rules.semi, undefined);
  174. // Semi-spacing is not enabled
  175. t.is(config.rules['semi-spacing'], undefined);
  176. });
  177. test('buildConfig: engines: undefined', t => {
  178. const config = manager.buildConfig({});
  179. // Do not include any Node.js version specific rules
  180. t.is(config.rules['prefer-spread'], undefined);
  181. t.is(config.rules['prefer-rest-params'], undefined);
  182. t.is(config.rules['prefer-destructuring'], undefined);
  183. t.is(config.rules['promise/prefer-await-to-then'], undefined);
  184. });
  185. test('buildConfig: engines: false', t => {
  186. const config = manager.buildConfig({engines: false});
  187. // Do not include any Node.js version specific rules
  188. t.is(config.rules['prefer-spread'], undefined);
  189. t.is(config.rules['prefer-rest-params'], undefined);
  190. t.is(config.rules['prefer-destructuring'], undefined);
  191. t.is(config.rules['promise/prefer-await-to-then'], undefined);
  192. });
  193. test('buildConfig: engines: invalid range', t => {
  194. const config = manager.buildConfig({engines: {node: '4'}});
  195. // Do not include any Node.js version specific rules
  196. t.is(config.rules['prefer-spread'], undefined);
  197. t.is(config.rules['prefer-rest-params'], undefined);
  198. t.is(config.rules['prefer-destructuring'], undefined);
  199. t.is(config.rules['promise/prefer-await-to-then'], undefined);
  200. });
  201. test('buildConfig: engines: >=8', t => {
  202. const config = manager.buildConfig({engines: {node: '>=8'}});
  203. // Include rules for Node.js 8 and above
  204. t.is(config.rules['promise/prefer-await-to-then'], 'error');
  205. });
  206. test('mergeWithPrettierConf: use `singleQuote`, `trailingComma`, `bracketSpacing` and `jsxBracketSameLine` from `prettier` config if defined', t => {
  207. const prettierOpts = {singleQuote: false, trailingComma: 'all', bracketSpacing: false, jsxBracketSameLine: false};
  208. const result = manager.mergeWithPrettierConf({}, prettierOpts);
  209. const expected = Object.assign({}, prettierOpts, {tabWidth: 2, useTabs: true, semi: true});
  210. t.deepEqual(result, expected);
  211. });
  212. test('mergeWithPrettierConf: determine `tabWidth`, `useTabs`, `semi` from xo config', t => {
  213. const prettierOpts = {tabWidth: 4, useTabs: false, semi: false};
  214. const result = manager.mergeWithPrettierConf({space: 4, semicolon: false}, {});
  215. const expected = Object.assign(
  216. {bracketSpacing: false, jsxBracketSameLine: false, singleQuote: true, trailingComma: 'none'},
  217. prettierOpts
  218. );
  219. t.deepEqual(result, expected);
  220. });
  221. test('mergeWithPrettierConf: determine `tabWidth`, `useTabs`, `semi` from prettier config', t => {
  222. const prettierOpts = {useTabs: false, semi: false, tabWidth: 4};
  223. const result = manager.mergeWithPrettierConf({}, prettierOpts);
  224. const expected = Object.assign(
  225. {bracketSpacing: false, jsxBracketSameLine: false, singleQuote: true, trailingComma: 'none'},
  226. prettierOpts
  227. );
  228. t.deepEqual(result, expected);
  229. });
  230. test('mergeWithPrettierConf: throw error is `semi`/`semicolon` conflicts', t => {
  231. t.throws(() => manager.mergeWithPrettierConf(
  232. {semicolon: true},
  233. {semi: false}
  234. ));
  235. t.throws(() => manager.mergeWithPrettierConf(
  236. {semicolon: false},
  237. {semi: true}
  238. ));
  239. t.notThrows(() => manager.mergeWithPrettierConf(
  240. {semicolon: true},
  241. {semi: true}
  242. ));
  243. t.notThrows(() => manager.mergeWithPrettierConf({semicolon: false}, {semi: false}));
  244. });
  245. test('mergeWithPrettierConf: throw error is `space`/`useTabs` conflicts', t => {
  246. t.throws(() => manager.mergeWithPrettierConf({space: false}, {useTabs: false}));
  247. t.throws(() => manager.mergeWithPrettierConf({space: true}, {useTabs: true}));
  248. t.notThrows(() => manager.mergeWithPrettierConf({space: 4}, {useTabs: false}));
  249. t.notThrows(() => manager.mergeWithPrettierConf({space: true}, {useTabs: false}));
  250. t.notThrows(() => manager.mergeWithPrettierConf({space: false}, {useTabs: true}));
  251. });
  252. test('mergeWithPrettierConf: throw error is `space`/`tabWidth` conflicts', t => {
  253. t.throws(() => manager.mergeWithPrettierConf({space: 4}, {tabWidth: 2}));
  254. t.throws(() => manager.mergeWithPrettierConf({space: 0}, {tabWidth: 2}));
  255. t.throws(() => manager.mergeWithPrettierConf({space: 2}, {tabWidth: 0}));
  256. t.notThrows(() => manager.mergeWithPrettierConf({space: 4}, {tabWidth: 4}));
  257. t.notThrows(() => manager.mergeWithPrettierConf({space: false}, {tabWidth: 4}));
  258. t.notThrows(() => manager.mergeWithPrettierConf({space: true}, {tabWidth: 4}));
  259. });
  260. test('buildConfig: rules', t => {
  261. const rules = {'object-curly-spacing': ['error', 'always']};
  262. const config = manager.buildConfig({rules});
  263. t.deepEqual(config.rules, rules);
  264. });
  265. test('buildConfig: parser', t => {
  266. const parser = 'babel-eslint';
  267. const config = manager.buildConfig({parser});
  268. t.deepEqual(config.baseConfig.parser, parser);
  269. });
  270. test('buildConfig: settings', t => {
  271. const settings = {'import/resolver': 'webpack'};
  272. const config = manager.buildConfig({settings});
  273. t.deepEqual(config.baseConfig.settings, settings);
  274. });
  275. test('buildConfig: extends', t => {
  276. const config = manager.buildConfig({extends: [
  277. 'plugin:foo/bar',
  278. 'eslint-config-foo-bar',
  279. 'foo-bar-two'
  280. ]});
  281. t.deepEqual(config.baseConfig.extends.slice(-3), [
  282. 'plugin:foo/bar',
  283. 'cwd/eslint-config-foo-bar',
  284. 'cwd/eslint-config-foo-bar-two'
  285. ]);
  286. });
  287. test('findApplicableOverrides', t => {
  288. const result = manager.findApplicableOverrides('/user/dir/foo.js', [
  289. {files: '**/f*.js'},
  290. {files: '**/bar.js'},
  291. {files: '**/*oo.js'},
  292. {files: '**/*.txt'}
  293. ]);
  294. t.is(result.hash, 0b1010);
  295. t.deepEqual(result.applicable, [
  296. {files: '**/f*.js'},
  297. {files: '**/*oo.js'}
  298. ]);
  299. });
  300. test('groupConfigs', t => {
  301. const paths = [
  302. '/user/foo/hello.js',
  303. '/user/foo/goodbye.js',
  304. '/user/foo/howdy.js',
  305. '/user/bar/hello.js'
  306. ];
  307. const opts = {
  308. esnext: false
  309. };
  310. const overrides = [
  311. {
  312. files: '**/foo/*',
  313. esnext: true
  314. },
  315. {
  316. files: '**/foo/howdy.js',
  317. space: 3,
  318. env: 'mocha'
  319. }
  320. ];
  321. const result = manager.groupConfigs(paths, opts, overrides);
  322. t.deepEqual(result, [
  323. {
  324. opts: {
  325. esnext: true
  326. },
  327. paths: ['/user/foo/hello.js', '/user/foo/goodbye.js']
  328. },
  329. {
  330. opts: {
  331. esnext: true,
  332. space: 3,
  333. envs: ['mocha']
  334. },
  335. paths: ['/user/foo/howdy.js']
  336. },
  337. {
  338. opts: {
  339. esnext: false
  340. },
  341. paths: ['/user/bar/hello.js']
  342. }
  343. ].map(obj => {
  344. obj.opts = Object.assign(manager.emptyOptions(), obj.opts);
  345. return obj;
  346. }));
  347. });
  348. test('mergeWithPkgConf: use child if closest', t => {
  349. const cwd = path.resolve('fixtures', 'nested', 'child');
  350. const result = manager.mergeWithPkgConf({cwd});
  351. const expected = Object.assign({}, childConfig.xo, {cwd}, {engines: {}});
  352. t.deepEqual(result, expected);
  353. });
  354. test('mergeWithPkgConf: use parent if closest', t => {
  355. const cwd = path.resolve('fixtures', 'nested');
  356. const result = manager.mergeWithPkgConf({cwd});
  357. const expected = Object.assign({}, parentConfig.xo, {cwd}, {engines: {}});
  358. t.deepEqual(result, expected);
  359. });
  360. test('mergeWithPkgConf: use parent if child is ignored', t => {
  361. const cwd = path.resolve('fixtures', 'nested', 'child-ignore');
  362. const result = manager.mergeWithPkgConf({cwd});
  363. const expected = Object.assign({}, parentConfig.xo, {cwd}, {engines: {}});
  364. t.deepEqual(result, expected);
  365. });
  366. test('mergeWithPkgConf: use child if child is empty', t => {
  367. const cwd = path.resolve('fixtures', 'nested', 'child-empty');
  368. const result = manager.mergeWithPkgConf({cwd});
  369. t.deepEqual(result, {cwd, engines: {}});
  370. });
  371. test('mergeWithPkgConf: read engines from package.json', t => {
  372. const cwd = path.resolve('fixtures', 'engines');
  373. const result = manager.mergeWithPkgConf({cwd});
  374. const expected = Object.assign({}, {engines: enginesConfig.engines}, {cwd});
  375. t.deepEqual(result, expected);
  376. });
  377. test('mergeWithPkgConf: XO engine options supersede package.json\'s', t => {
  378. const cwd = path.resolve('fixtures', 'engines');
  379. const result = manager.mergeWithPkgConf({cwd, engines: {node: '>=8'}});
  380. const expected = Object.assign({}, {engines: {node: '>=8'}}, {cwd});
  381. t.deepEqual(result, expected);
  382. });
  383. test('mergeWithPkgConf: XO engine options false supersede package.json\'s', t => {
  384. const cwd = path.resolve('fixtures', 'engines');
  385. const result = manager.mergeWithPkgConf({cwd, engines: false});
  386. const expected = Object.assign({}, {engines: false}, {cwd});
  387. t.deepEqual(result, expected);
  388. });