index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. "use strict";
  2. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  3. if (k2 === undefined) k2 = k;
  4. var desc = Object.getOwnPropertyDescriptor(m, k);
  5. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  6. desc = { enumerable: true, get: function() { return m[k]; } };
  7. }
  8. Object.defineProperty(o, k2, desc);
  9. }) : (function(o, m, k, k2) {
  10. if (k2 === undefined) k2 = k;
  11. o[k2] = m[k];
  12. }));
  13. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  14. Object.defineProperty(o, "default", { enumerable: true, value: v });
  15. }) : function(o, v) {
  16. o["default"] = v;
  17. });
  18. var __importStar = (this && this.__importStar) || function (mod) {
  19. if (mod && mod.__esModule) return mod;
  20. var result = {};
  21. if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
  22. __setModuleDefault(result, mod);
  23. return result;
  24. };
  25. var __importDefault = (this && this.__importDefault) || function (mod) {
  26. return (mod && mod.__esModule) ? mod : { "default": mod };
  27. };
  28. Object.defineProperty(exports, "__esModule", { value: true });
  29. exports.parseSync = exports.parse = exports.parseFromFilesSync = exports.parseFromFiles = exports.parseString = exports.parseBuffer = void 0;
  30. const fs = __importStar(require("fs"));
  31. const path = __importStar(require("path"));
  32. const semver = __importStar(require("semver"));
  33. const minimatch_1 = require("minimatch");
  34. const wasm_1 = require("@one-ini/wasm");
  35. // @ts-ignore So we can set the rootDir to be 'lib', without processing
  36. // package.json
  37. const package_json_1 = __importDefault(require("../package.json"));
  38. const escapedSep = new RegExp(path.sep.replace(/\\/g, '\\\\'), 'g');
  39. const matchOptions = { matchBase: true, dot: true, noext: true };
  40. // These are specified by the editorconfig script
  41. /* eslint-disable @typescript-eslint/naming-convention */
  42. const knownProps = {
  43. end_of_line: true,
  44. indent_style: true,
  45. indent_size: true,
  46. insert_final_newline: true,
  47. trim_trailing_whitespace: true,
  48. charset: true,
  49. };
  50. /**
  51. * Parse a buffer using the faster one-ini WASM approach into something
  52. * relatively easy to deal with in JS.
  53. *
  54. * @param data UTF8-encoded bytes.
  55. * @returns Parsed contents. Will be truncated if there was a parse error.
  56. */
  57. function parseBuffer(data) {
  58. const parsed = (0, wasm_1.parse_to_uint32array)(data);
  59. let cur = {};
  60. const res = [[null, cur]];
  61. let key = null;
  62. for (let i = 0; i < parsed.length; i += 3) {
  63. switch (parsed[i]) {
  64. case wasm_1.TokenTypes.Section: {
  65. cur = {};
  66. res.push([
  67. data.toString('utf8', parsed[i + 1], parsed[i + 2]),
  68. cur,
  69. ]);
  70. break;
  71. }
  72. case wasm_1.TokenTypes.Key:
  73. key = data.toString('utf8', parsed[i + 1], parsed[i + 2]);
  74. break;
  75. case wasm_1.TokenTypes.Value: {
  76. cur[key] = data.toString('utf8', parsed[i + 1], parsed[i + 2]);
  77. break;
  78. }
  79. default: // Comments, etc.
  80. break;
  81. }
  82. }
  83. return res;
  84. }
  85. exports.parseBuffer = parseBuffer;
  86. /**
  87. * Parses a string. If possible, you should always use ParseBuffer instead,
  88. * since this function does a UTF16-to-UTF8 conversion first.
  89. *
  90. * @param data String to parse.
  91. * @returns Parsed contents. Will be truncated if there was a parse error.
  92. * @deprecated Use {@link ParseBuffer} instead.
  93. */
  94. function parseString(data) {
  95. return parseBuffer(Buffer.from(data));
  96. }
  97. exports.parseString = parseString;
  98. /**
  99. * Gets a list of *potential* filenames based on the path of the target
  100. * filename.
  101. *
  102. * @param filepath File we are asking about.
  103. * @param options Config file name and root directory
  104. * @returns List of potential fully-qualified filenames that might have configs.
  105. */
  106. function getConfigFileNames(filepath, options) {
  107. const paths = [];
  108. do {
  109. filepath = path.dirname(filepath);
  110. paths.push(path.join(filepath, options.config));
  111. } while (filepath !== options.root);
  112. return paths;
  113. }
  114. /**
  115. * Take a combined config for the target file, and tweak it slightly based on
  116. * which editorconfig version's rules we are using.
  117. *
  118. * @param matches Combined config.
  119. * @param version Editorconfig version to enforce.
  120. * @returns The passed-in matches object, modified in place.
  121. */
  122. function processMatches(matches, version) {
  123. // Set indent_size to 'tab' if indent_size is unspecified and
  124. // indent_style is set to 'tab'.
  125. if ('indent_style' in matches
  126. && matches.indent_style === 'tab'
  127. && !('indent_size' in matches)
  128. && semver.gte(version, '0.10.0')) {
  129. matches.indent_size = 'tab';
  130. }
  131. // Set tab_width to indent_size if indent_size is specified and
  132. // tab_width is unspecified
  133. if ('indent_size' in matches
  134. && !('tab_width' in matches)
  135. && matches.indent_size !== 'tab') {
  136. matches.tab_width = matches.indent_size;
  137. }
  138. // Set indent_size to tab_width if indent_size is 'tab'
  139. if ('indent_size' in matches
  140. && 'tab_width' in matches
  141. && matches.indent_size === 'tab') {
  142. matches.indent_size = matches.tab_width;
  143. }
  144. return matches;
  145. }
  146. function buildFullGlob(pathPrefix, glob) {
  147. switch (glob.indexOf('/')) {
  148. case -1:
  149. glob = '**/' + glob;
  150. break;
  151. case 0:
  152. glob = glob.substring(1);
  153. break;
  154. default:
  155. break;
  156. }
  157. // braces_escaped_backslash2
  158. // backslash_not_on_windows
  159. glob = glob.replace(/\\\\/g, '\\\\\\\\');
  160. // star_star_over_separator{1,3,5,6,9,15}
  161. glob = glob.replace(/\*\*/g, '{*,**/**/**}');
  162. // NOT path.join. Must stay in forward slashes.
  163. return new minimatch_1.Minimatch(`${pathPrefix}/${glob}`, matchOptions);
  164. }
  165. /**
  166. * Normalize the properties read from a config file so that their key names
  167. * are lowercased for the known properties, and their values are parsed into
  168. * the correct JS types if possible.
  169. *
  170. * @param options
  171. * @returns
  172. */
  173. function normalizeProps(options) {
  174. const props = {};
  175. for (const key in options) {
  176. if (options.hasOwnProperty(key)) {
  177. const value = options[key];
  178. const key2 = key.toLowerCase();
  179. let value2 = value;
  180. // @ts-ignore -- Fix types here
  181. if (knownProps[key2]) {
  182. // All of the values for the known props are lowercase.
  183. value2 = String(value).toLowerCase();
  184. }
  185. try {
  186. value2 = JSON.parse(String(value));
  187. }
  188. catch (e) { }
  189. if (typeof value2 === 'undefined' || value2 === null) {
  190. // null and undefined are values specific to JSON (no special meaning
  191. // in editorconfig) & should just be returned as regular strings.
  192. value2 = String(value);
  193. }
  194. // @ts-ignore -- Fix types here
  195. props[key2] = value2;
  196. }
  197. }
  198. return props;
  199. }
  200. /**
  201. * Take the contents of a config file, and prepare it for use. If a cache is
  202. * provided, the result will be stored there. As such, all of the higher-CPU
  203. * work that is per-file should be done here.
  204. *
  205. * @param filepath The fully-qualified path of the file.
  206. * @param contents The contents as read from that file.
  207. * @param options Access to the cache.
  208. * @returns Processed file with globs pre-computed.
  209. */
  210. function processFileContents(filepath, contents, options) {
  211. let res;
  212. if (!contents) {
  213. // Negative cache
  214. res = {
  215. root: false,
  216. notfound: true,
  217. name: filepath,
  218. config: [[null, {}, null]],
  219. };
  220. }
  221. else {
  222. let pathPrefix = path.dirname(filepath);
  223. if (path.sep !== '/') {
  224. // Windows-only
  225. pathPrefix = pathPrefix.replace(escapedSep, '/');
  226. }
  227. // After Windows path backslash's are turned into slashes, so that
  228. // the backslashes we add here aren't turned into forward slashes:
  229. // All of these characters are special to minimatch, but can be
  230. // forced into path names on many file systems. Escape them. Note
  231. // that these are in the order of the case statement in minimatch.
  232. pathPrefix = pathPrefix.replace(/[?*+@!()|[\]{}]/g, '\\$&');
  233. // I can't think of a way for this to happen in the filesystems I've
  234. // seen (because of the path.dirname above), but let's be thorough.
  235. pathPrefix = pathPrefix.replace(/^#/, '\\#');
  236. const globbed = parseBuffer(contents).map(([name, body]) => [
  237. name,
  238. normalizeProps(body),
  239. name ? buildFullGlob(pathPrefix, name) : null,
  240. ]);
  241. res = {
  242. root: !!globbed[0][1].root,
  243. name: filepath,
  244. config: globbed,
  245. };
  246. }
  247. if (options.cache) {
  248. options.cache.set(filepath, res);
  249. }
  250. return res;
  251. }
  252. /**
  253. * Get a file from the cache, or read its contents from disk, process, and
  254. * insert into the cache (if configured).
  255. *
  256. * @param filepath The fully-qualified path of the config file.
  257. * @param options Access to the cache, if configured.
  258. * @returns The processed file, or undefined if there was an error reading it.
  259. */
  260. async function getConfig(filepath, options) {
  261. if (options.cache) {
  262. const cached = options.cache.get(filepath);
  263. if (cached) {
  264. return cached;
  265. }
  266. }
  267. const contents = await new Promise(resolve => {
  268. fs.readFile(filepath, (_, buf) => {
  269. // Ignore errors. contents will be undefined
  270. // Perhaps only file-not-found should be ignored?
  271. resolve(buf);
  272. });
  273. });
  274. return processFileContents(filepath, contents, options);
  275. }
  276. /**
  277. * Get a file from the cache, or read its contents from disk, process, and
  278. * insert into the cache (if configured). Synchronous.
  279. *
  280. * @param filepath The fully-qualified path of the config file.
  281. * @param options Access to the cache, if configured.
  282. * @returns The processed file, or undefined if there was an error reading it.
  283. */
  284. function getConfigSync(filepath, options) {
  285. if (options.cache) {
  286. const cached = options.cache.get(filepath);
  287. if (cached) {
  288. return cached;
  289. }
  290. }
  291. let contents;
  292. try {
  293. contents = fs.readFileSync(filepath);
  294. }
  295. catch (_) {
  296. // Ignore errors
  297. // Perhaps only file-not-found should be ignored
  298. }
  299. return processFileContents(filepath, contents, options);
  300. }
  301. /**
  302. * Get all of the possibly-existing config files, stopping when one is marked
  303. * root=true.
  304. *
  305. * @param files List of potential files
  306. * @param options Access to cache if configured
  307. * @returns List of processed configs for existing files
  308. */
  309. async function getAllConfigs(files, options) {
  310. const configs = [];
  311. for (const file of files) {
  312. const config = await getConfig(file, options);
  313. if (!config.notfound) {
  314. configs.push(config);
  315. if (config.root) {
  316. break;
  317. }
  318. }
  319. }
  320. return configs;
  321. }
  322. /**
  323. * Get all of the possibly-existing config files, stopping when one is marked
  324. * root=true. Synchronous.
  325. *
  326. * @param files List of potential files
  327. * @param options Access to cache if configured
  328. * @returns List of processed configs for existing files
  329. */
  330. function getAllConfigsSync(files, options) {
  331. const configs = [];
  332. for (const file of files) {
  333. const config = getConfigSync(file, options);
  334. if (!config.notfound) {
  335. configs.push(config);
  336. if (config.root) {
  337. break;
  338. }
  339. }
  340. }
  341. return configs;
  342. }
  343. /**
  344. * Normalize the options passed in to the publicly-visible functions.
  345. *
  346. * @param filepath The name of the target file, relative to process.cwd().
  347. * @param options Potentially-incomplete options.
  348. * @returns The fully-qualified target file name and the normalized options.
  349. */
  350. function opts(filepath, options = {}) {
  351. const resolvedFilePath = path.resolve(filepath);
  352. return [
  353. resolvedFilePath,
  354. {
  355. config: options.config || '.editorconfig',
  356. version: options.version || package_json_1.default.version,
  357. root: path.resolve(options.root || path.parse(resolvedFilePath).root),
  358. files: options.files,
  359. cache: options.cache,
  360. },
  361. ];
  362. }
  363. /**
  364. * Low-level interface, which exists only for backward-compatibility.
  365. * Deprecated.
  366. *
  367. * @param filepath The name of the target file, relative to process.cwd().
  368. * @param files A promise for a list of objects describing the files.
  369. * @param options All options
  370. * @returns The properties found for filepath
  371. * @deprecated
  372. */
  373. async function parseFromFiles(filepath, files, options = {}) {
  374. return parseFromFilesSync(filepath, await files, options);
  375. }
  376. exports.parseFromFiles = parseFromFiles;
  377. /**
  378. * Low-level interface, which exists only for backward-compatibility.
  379. * Deprecated.
  380. *
  381. * @param filepath The name of the target file, relative to process.cwd().
  382. * @param files A list of objects describing the files.
  383. * @param options All options
  384. * @returns The properties found for filepath
  385. * @deprecated
  386. */
  387. function parseFromFilesSync(filepath, files, options = {}) {
  388. const [resolvedFilePath, processedOptions] = opts(filepath, options);
  389. const configs = [];
  390. for (const ecf of files) {
  391. let cfg;
  392. if (!options.cache || !(cfg = options.cache.get(ecf.name))) { // Single "="!
  393. cfg = processFileContents(ecf.name, ecf.contents, processedOptions);
  394. }
  395. if (!cfg.notfound) {
  396. configs.push(cfg);
  397. }
  398. if (cfg.root) {
  399. break;
  400. }
  401. }
  402. return combine(resolvedFilePath, configs, processedOptions);
  403. }
  404. exports.parseFromFilesSync = parseFromFilesSync;
  405. /**
  406. * Combine the pre-parsed results of all matching config file sections, in
  407. * order.
  408. *
  409. * @param filepath The target file path
  410. * @param configs All of the found config files, up to the root
  411. * @param options Adds to `options.files` if it exists
  412. * @returns Combined properties
  413. */
  414. function combine(filepath, configs, options) {
  415. const ret = configs.reverse().reduce((props, processed) => {
  416. for (const [name, body, glob] of processed.config) {
  417. if (glob && glob.match(filepath)) {
  418. Object.assign(props, body);
  419. if (options.files) {
  420. options.files.push({
  421. fileName: processed.name,
  422. glob: name,
  423. });
  424. }
  425. }
  426. }
  427. return props;
  428. }, {});
  429. return processMatches(ret, options.version);
  430. }
  431. /**
  432. * Find all of the properties from matching sections in config files in the
  433. * same directory or toward the root of the filesystem.
  434. *
  435. * @param filepath The target file name, relative to process.cwd().
  436. * @param options All options
  437. * @returns Combined properties for the target file
  438. */
  439. async function parse(filepath, options = {}) {
  440. const [resolvedFilePath, processedOptions] = opts(filepath, options);
  441. const filepaths = getConfigFileNames(resolvedFilePath, processedOptions);
  442. const configs = await getAllConfigs(filepaths, processedOptions);
  443. return combine(resolvedFilePath, configs, processedOptions);
  444. }
  445. exports.parse = parse;
  446. /**
  447. * Find all of the properties from matching sections in config files in the
  448. * same directory or toward the root of the filesystem. Synchronous.
  449. *
  450. * @param filepath The target file name, relative to process.cwd().
  451. * @param options All options
  452. * @returns Combined properties for the target file
  453. */
  454. function parseSync(filepath, options = {}) {
  455. const [resolvedFilePath, processedOptions] = opts(filepath, options);
  456. const filepaths = getConfigFileNames(resolvedFilePath, processedOptions);
  457. const configs = getAllConfigsSync(filepaths, processedOptions);
  458. return combine(resolvedFilePath, configs, processedOptions);
  459. }
  460. exports.parseSync = parseSync;