editorconfig 3.0.2 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -72,13 +72,13 @@ const knownProps = new Set(knownPropNames);
72
72
  */
73
73
  function parseBuffer(data) {
74
74
  const parsed = (0, wasm_1.parse_to_uint32array)(data);
75
- let cur = {};
75
+ let cur = Object.create(null);
76
76
  const res = [[null, cur]];
77
77
  let key = null;
78
78
  for (let i = 0; i < parsed.length; i += 3) {
79
79
  switch (parsed[i]) {
80
80
  case wasm_1.TokenTypes.Section: {
81
- cur = {};
81
+ cur = Object.create(null);
82
82
  res.push([
83
83
  data.toString('utf8', parsed[i + 1], parsed[i + 2]),
84
84
  cur,
@@ -122,7 +122,7 @@ function getConfigFileNames(filepath, options) {
122
122
  do {
123
123
  filepath = path.dirname(filepath);
124
124
  paths.push(path.join(filepath, options.config));
125
- } while (filepath !== options.root);
125
+ } while (filepath !== options.root && filepath !== path.dirname(filepath));
126
126
  return paths;
127
127
  }
128
128
  /**
@@ -187,18 +187,18 @@ function buildFullGlob(pathPrefix, glob) {
187
187
  * @returns
188
188
  */
189
189
  function normalizeProps(options) {
190
- const props = {};
190
+ const props = Object.create(null);
191
191
  for (const key in options) {
192
192
  if (Object.prototype.hasOwnProperty.call(options, key)) {
193
193
  const value = options[key];
194
194
  const key2 = key.toLowerCase();
195
- let value2 = value;
196
- if (knownProps.has(key2)) {
197
- // All of the values for the known props are lowercase.
198
- value2 = String(value).toLowerCase();
199
- }
195
+ // All of the values for the known props are lowercase.
196
+ const str = knownProps.has(key2) ?
197
+ String(value).toLowerCase() :
198
+ String(value);
199
+ let value2 = str;
200
200
  try {
201
- value2 = JSON.parse(String(value));
201
+ value2 = JSON.parse(str);
202
202
  }
203
203
  catch (_e) {
204
204
  // Ignored
@@ -207,13 +207,57 @@ function normalizeProps(options) {
207
207
  //
208
208
  // null and undefined are values specific to JSON (no special meaning
209
209
  // in editorconfig) & should just be returned as regular strings.
210
- value2 = String(value);
210
+ value2 = str;
211
211
  }
212
212
  props[key2] = value2;
213
213
  }
214
214
  }
215
215
  return props;
216
216
  }
217
+ /**
218
+ * Section globs are matched against paths that are already relative to the
219
+ * directory holding the `.editorconfig` file, so no glob can ever refer to a
220
+ * parent directory. The glob engines do not resolve a `..` component against
221
+ * that path though; they let it consume any one directory level. That makes a
222
+ * section such as `[dir/../x.c]` match `x.c` sitting next to the config file,
223
+ * even though `x.c` is not inside `dir`. Detect those components so the
224
+ * section can be rejected.
225
+ *
226
+ * @param glob Section name, as written in the config file.
227
+ * @returns True if the glob has an unescaped `..` path component.
228
+ */
229
+ function hasParentDirComponent(glob) {
230
+ return glob.split('/').some(part => {
231
+ let name = '';
232
+ for (let i = 0; i < part.length; i++) {
233
+ if (part[i] === '\\') {
234
+ // A backslash escapes the next character, so it is a literal.
235
+ i++;
236
+ }
237
+ else {
238
+ name += part[i];
239
+ }
240
+ }
241
+ return name === '..';
242
+ });
243
+ }
244
+ /**
245
+ * Build the matcher for a single section of a config file.
246
+ *
247
+ * The global section has no name and never matches a file, and a section whose
248
+ * glob names a parent directory cannot match a file below the config's own
249
+ * directory, so both get no matcher at all.
250
+ *
251
+ * @param pathPrefix Escaped path of the directory holding the config file.
252
+ * @param name Section name, as written in the config file.
253
+ * @returns The section's matcher, or null if it cannot match anything.
254
+ */
255
+ function buildSectionGlob(pathPrefix, name) {
256
+ if (!name || hasParentDirComponent(name)) {
257
+ return null;
258
+ }
259
+ return buildFullGlob(pathPrefix, name);
260
+ }
217
261
  /**
218
262
  * Take the contents of a config file, and prepare it for use. If a cache is
219
263
  * provided, the result will be stored there. As such, all of the higher-CPU
@@ -233,7 +277,7 @@ function processFileContents(filepath, contents, options) {
233
277
  root: false,
234
278
  notfound: true,
235
279
  name: filepath,
236
- config: [[null, {}, null]],
280
+ config: [[null, Object.create(null), null]],
237
281
  };
238
282
  }
239
283
  else {
@@ -248,7 +292,7 @@ function processFileContents(filepath, contents, options) {
248
292
  const globbed = parseBuffer(contents).map(([name, body]) => [
249
293
  name,
250
294
  normalizeProps(body),
251
- name ? buildFullGlob(pathPrefix, name) : null,
295
+ buildSectionGlob(pathPrefix, name),
252
296
  ]);
253
297
  res = {
254
298
  root: Boolean(globbed[0][1].root), // Global section: globbed[0]
@@ -262,20 +306,20 @@ function processFileContents(filepath, contents, options) {
262
306
  return res;
263
307
  }
264
308
  /**
265
- * Get a file from the cache, or read its contents from disk, process, and
266
- * insert into the cache (if configured).
309
+ * Reads of config files that are in progress, per cache, so that concurrent
310
+ * lookups of the same file share one read. Entries are removed once the read
311
+ * settles; the finished result lives in the cache.
312
+ */
313
+ const pendingConfigs = new WeakMap();
314
+ /**
315
+ * Read a config file's contents from disk, process, and insert into the cache
316
+ * (if configured).
267
317
  *
268
318
  * @param filepath The fully-qualified path of the config file.
269
319
  * @param options Access to the cache, if configured.
270
- * @returns The processed file, or undefined if there was an error reading it.
320
+ * @returns The processed file.
271
321
  */
272
- async function getConfig(filepath, options) {
273
- if (options.cache) {
274
- const cached = options.cache.get(filepath);
275
- if (cached) {
276
- return cached;
277
- }
278
- }
322
+ async function readConfig(filepath, options) {
279
323
  const contents = await new Promise(resolve => {
280
324
  fs.readFile(filepath, (_, buf) => {
281
325
  // Ignore errors. contents will be undefined
@@ -285,6 +329,46 @@ async function getConfig(filepath, options) {
285
329
  });
286
330
  return processFileContents(filepath, contents, options);
287
331
  }
332
+ /**
333
+ * Get a file from the cache, or read its contents from disk, process, and
334
+ * insert into the cache (if configured). If the cache is configured,
335
+ * concurrent calls for the same file share one read.
336
+ *
337
+ * @param filepath The fully-qualified path of the config file.
338
+ * @param options Access to the cache, if configured.
339
+ * @returns The processed file, or undefined if there was an error reading it.
340
+ */
341
+ async function getConfig(filepath, options) {
342
+ if (!options.cache) {
343
+ return readConfig(filepath, options);
344
+ }
345
+ const cached = options.cache.get(filepath);
346
+ if (cached) {
347
+ return cached;
348
+ }
349
+ let pending = pendingConfigs.get(options.cache);
350
+ let config = undefined;
351
+ if (pending) {
352
+ config = pending.get(filepath);
353
+ }
354
+ else {
355
+ pending = new Map();
356
+ pendingConfigs.set(options.cache, pending);
357
+ }
358
+ if (!config) {
359
+ config = readConfig(filepath, options);
360
+ pending.set(filepath, config);
361
+ try {
362
+ // Reminder: this returns immediately with a promise, and the finally
363
+ // block doesn't fire until the promise is settled.
364
+ return await config;
365
+ }
366
+ finally {
367
+ pending.delete(filepath);
368
+ }
369
+ }
370
+ return config;
371
+ }
288
372
  /**
289
373
  * Get a file from the cache, or read its contents from disk, process, and
290
374
  * insert into the cache (if configured). Synchronous.
@@ -412,7 +496,7 @@ function combine(filepath, configs, options) {
412
496
  }
413
497
  }
414
498
  return props;
415
- }, {});
499
+ }, Object.create(null));
416
500
  if (options.unset) {
417
501
  unset(ret);
418
502
  }
@@ -1 +1 @@
1
- import 'chai/register-should.js';
1
+ export {};
package/lib/index.test.js CHANGED
@@ -32,15 +32,21 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  /* eslint-disable @typescript-eslint/no-deprecated */
37
- require("chai/register-should.js");
38
40
  const editorconfig = __importStar(require("./index.js"));
39
41
  const fs = __importStar(require("node:fs"));
42
+ const os = __importStar(require("node:os"));
40
43
  const path = __importStar(require("node:path"));
44
+ const chai_1 = require("chai");
41
45
  const node_buffer_1 = require("node:buffer");
46
+ const node_process_1 = __importDefault(require("node:process"));
47
+ (0, chai_1.should)();
42
48
  describe('parse', () => {
43
- const expected = {
49
+ const expected = Object.assign(Object.create(null), {
44
50
  indent_style: 'space',
45
51
  indent_size: 2,
46
52
  end_of_line: 'lf',
@@ -51,24 +57,43 @@ describe('parse', () => {
51
57
  block_comment: '*',
52
58
  block_comment_end: '*/',
53
59
  block_comment_start: '/**',
54
- };
60
+ });
55
61
  const target = path.join(__dirname, '/app.js');
56
62
  it('async', async () => {
57
63
  const cfg = await editorconfig.parse(target);
58
- cfg.should.eql(expected);
64
+ (0, chai_1.expect)(cfg).to.eql(expected);
59
65
  });
60
66
  it('sync', () => {
61
67
  const visited = [];
62
68
  const cfg = editorconfig.parseSync(target, { files: visited });
63
- cfg.should.eql(expected);
69
+ (0, chai_1.expect)(cfg).to.eql(expected);
64
70
  visited.should.have.lengthOf(1);
65
71
  visited[0].glob.should.eql('*');
66
72
  visited[0].fileName.should.match(/\.editorconfig$/);
67
73
  });
74
+ it('stops at the filesystem root when the configured root is not an ancestor', async () => {
75
+ const root = path.join(__dirname, 'not-an-ancestor');
76
+ const cfg = await editorconfig.parse(target, { root });
77
+ (0, chai_1.expect)(cfg).to.eql(expected);
78
+ });
79
+ it('stops at the filesystem root synchronously when the configured root is not an ancestor', () => {
80
+ const root = path.join(__dirname, 'not-an-ancestor');
81
+ const cfg = editorconfig.parseSync(target, { root });
82
+ (0, chai_1.expect)(cfg).to.eql(expected);
83
+ });
84
+ const windowsIt = node_process_1.default.platform === 'win32' ? it : it.skip;
85
+ windowsIt('parses Windows extended-length paths', async () => {
86
+ const cfg = await editorconfig.parse(path.toNamespacedPath(target));
87
+ (0, chai_1.expect)(cfg).to.eql(expected);
88
+ });
89
+ windowsIt('parses Windows extended-length paths synchronously', () => {
90
+ const cfg = editorconfig.parseSync(path.toNamespacedPath(target));
91
+ (0, chai_1.expect)(cfg).to.eql(expected);
92
+ });
68
93
  it('caches', async () => {
69
94
  const cache = new Map();
70
95
  const cfg = await editorconfig.parse(target, { cache });
71
- cfg.should.eql(expected);
96
+ (0, chai_1.expect)(cfg).to.eql(expected);
72
97
  cache.size.should.be.eql(2);
73
98
  await editorconfig.parse(target, { cache });
74
99
  cache.size.should.be.eql(2);
@@ -76,11 +101,28 @@ describe('parse', () => {
76
101
  it('caches sync', () => {
77
102
  const cache = new Map();
78
103
  const cfg = editorconfig.parseSync(target, { cache });
79
- cfg.should.eql(expected);
104
+ (0, chai_1.expect)(cfg).to.eql(expected);
80
105
  cache.size.should.be.eql(2);
81
106
  editorconfig.parseSync(target, { cache });
82
107
  cache.size.should.be.eql(2);
83
108
  });
109
+ it('shares concurrent reads through the cache', async () => {
110
+ const cache = new Map();
111
+ let sets = 0;
112
+ const counting = {
113
+ get: p => cache.get(p),
114
+ set(p, config) {
115
+ sets++;
116
+ cache.set(p, config);
117
+ return this;
118
+ },
119
+ };
120
+ const cfgs = await Promise.all(Array.from({ length: 10 }, () => editorconfig.parse(target, { cache: counting })));
121
+ for (const cfg of cfgs) {
122
+ (0, chai_1.expect)(cfg).to.eql(expected);
123
+ }
124
+ sets.should.be.eql(2);
125
+ });
84
126
  });
85
127
  describe('parseFromFiles', () => {
86
128
  const expected = {
@@ -108,35 +150,35 @@ describe('parseFromFiles', () => {
108
150
  ];
109
151
  it('async', async () => {
110
152
  const cfg = await editorconfig.parseFromFiles(target, Promise.resolve(configs));
111
- cfg.should.eql(expected);
153
+ (0, chai_1.expect)(cfg).to.eql(expected);
112
154
  });
113
155
  it('sync', () => {
114
156
  const cfg = editorconfig.parseFromFilesSync(target, configs);
115
- cfg.should.eql(expected);
157
+ (0, chai_1.expect)(cfg).to.eql(expected);
116
158
  });
117
159
  it('handles null', () => {
118
160
  const cfg = editorconfig.parseFromFilesSync(target, [{
119
161
  name: configPath,
120
162
  contents: node_buffer_1.Buffer.from('[*]\nfoo = null\n'),
121
163
  }]);
122
- cfg.should.eql({ foo: 'null' });
164
+ (0, chai_1.expect)(cfg).to.eql({ foo: 'null' });
123
165
  });
124
166
  it('caches async', async () => {
125
167
  const cache = new Map();
126
168
  const cfg = await editorconfig.parseFromFiles(target, Promise.resolve(configs2), { cache });
127
- cfg.should.eql(expected);
169
+ (0, chai_1.expect)(cfg).to.eql(expected);
128
170
  cache.size.should.be.eql(2);
129
171
  const cfg2 = await editorconfig.parseFromFiles(target, Promise.resolve(configs2), { cache });
130
- cfg2.should.eql(expected);
172
+ (0, chai_1.expect)(cfg2).to.eql(expected);
131
173
  cache.size.should.be.eql(2);
132
174
  });
133
175
  it('caches sync', () => {
134
176
  const cache = new Map();
135
177
  const cfg = editorconfig.parseFromFilesSync(target, configs2, { cache });
136
- cfg.should.eql(expected);
178
+ (0, chai_1.expect)(cfg).to.eql(expected);
137
179
  cache.size.should.be.eql(2);
138
180
  const cfg2 = editorconfig.parseFromFilesSync(target, configs2, { cache });
139
- cfg2.should.eql(expected);
181
+ (0, chai_1.expect)(cfg2).to.eql(expected);
140
182
  cache.size.should.be.eql(2);
141
183
  });
142
184
  it('handles minimatch escapables', () => {
@@ -153,7 +195,7 @@ describe('parseFromFiles', () => {
153
195
  ];
154
196
  const escTarget = `${bogusPath}/app.js`;
155
197
  const cfg = editorconfig.parseFromFilesSync(escTarget, escConfigs);
156
- cfg.should.eql(expected);
198
+ (0, chai_1.expect)(cfg).to.eql(expected);
157
199
  });
158
200
  });
159
201
  describe('parseString', () => {
@@ -176,19 +218,70 @@ describe('parseString', () => {
176
218
  const contents = fs.readFileSync(configPath, 'utf8');
177
219
  it('sync', () => {
178
220
  const cfg = editorconfig.parseString(contents);
179
- cfg.should.eql(expected);
221
+ (0, chai_1.expect)(cfg).to.eql(expected);
180
222
  });
181
223
  it('handles errors', () => {
182
224
  const cfg = editorconfig.parseString('root: ');
183
- cfg.should.eql([[null, {}]]);
225
+ (0, chai_1.expect)(cfg).to.eql([[null, Object.create(null)]]);
184
226
  });
185
227
  it('handles backslashes in glob', () => {
186
228
  const cfg = editorconfig.parseString('[a\\\\b]');
187
- cfg.should.eql([[null, {}], ['a\\\\b', {}]]);
229
+ (0, chai_1.expect)(cfg).to.eql([[null, {}], ['a\\\\b', Object.create(null)]]);
188
230
  });
189
231
  it('handles blank comments', () => {
190
232
  const cfg = editorconfig.parseString('#');
191
- cfg.should.eql([[null, {}]]);
233
+ (0, chai_1.expect)(cfg).to.eql([[null, Object.create(null)]]);
234
+ });
235
+ it('handles __proto__', () => {
236
+ const cfg = editorconfig.parseString('__proto__ = foo');
237
+ const obj = Object.create(null);
238
+ // eslint-disable-next-line no-proto
239
+ obj.__proto__ = 'foo';
240
+ (0, chai_1.expect)(cfg).to.eql([[null, obj]]);
241
+ });
242
+ });
243
+ describe('parent directory components in section globs', () => {
244
+ let dir = '';
245
+ let target = '';
246
+ before(() => {
247
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'editorconfig-'));
248
+ target = path.join(dir, 'x.c');
249
+ fs.writeFileSync(target, '');
250
+ });
251
+ after(() => {
252
+ fs.rmSync(dir, { recursive: true, force: true });
253
+ });
254
+ /**
255
+ * Write a one-section config and return the properties for `x.c`.
256
+ *
257
+ * @param glob Section name to test.
258
+ * @returns Properties found for the target file.
259
+ */
260
+ function configFor(glob) {
261
+ fs.writeFileSync(path.join(dir, '.editorconfig'), `root = true\n\n[${glob}]\nhit = yes\n`);
262
+ return editorconfig.parseSync(target);
263
+ }
264
+ it('does not let a `..` component consume a directory', () => {
265
+ // `[dir/../x.c]` must not match `x.c`, which is not inside `dir`.
266
+ (0, chai_1.expect)(configFor('dir/../x.c')).to.not.have.property('hit');
267
+ (0, chai_1.expect)(configFor('a/../b/../x.c')).to.not.have.property('hit');
268
+ (0, chai_1.expect)(configFor('**/../x.c')).to.not.have.property('hit');
269
+ (0, chai_1.expect)(configFor('../x.c')).to.not.have.property('hit');
270
+ (0, chai_1.expect)(configFor('./x.c')).to.not.have.property('hit');
271
+ });
272
+ it('still matches a plain name and a `**` pattern', () => {
273
+ (0, chai_1.expect)(configFor('x.c')).to.have.property('hit', 'yes');
274
+ (0, chai_1.expect)(configFor('**/x.c')).to.have.property('hit', 'yes');
275
+ (0, chai_1.expect)(configFor('*.c')).to.have.property('hit', 'yes');
276
+ });
277
+ it('treats an escaped `..` as a literal name', () => {
278
+ // `a\.\.` is a file called `a..`, not a parent-directory reference.
279
+ (0, chai_1.expect)(configFor('a\\.\\.c')).to.not.have.property('hit');
280
+ });
281
+ it('does not match through the async parse either', async () => {
282
+ fs.writeFileSync(path.join(dir, '.editorconfig'), 'root = true\n\n[dir/../x.c]\nhit = yes\n');
283
+ const cfg = await editorconfig.parse(target);
284
+ (0, chai_1.expect)(cfg).to.not.have.property('hit');
192
285
  });
193
286
  });
194
287
  describe('extra behavior', () => {
@@ -202,12 +295,29 @@ indent_size = 4
202
295
 
203
296
  [!(package).json]
204
297
  indent_size = 3`));
205
- matcher(path.join(__dirname, 'package.json')).should.include({ indent_size: 4 });
206
- matcher(path.join(__dirname, 'foo.json')).should.include({ indent_size: 3 });
298
+ (0, chai_1.expect)(matcher(path.join(__dirname, 'package.json'))).to.include({ indent_size: 4 });
299
+ (0, chai_1.expect)(matcher(path.join(__dirname, 'foo.json'))).to.include({ indent_size: 3 });
300
+ });
301
+ it('parses known boolean values in any case', () => {
302
+ const matcher = editorconfig.matcher({
303
+ root: __dirname,
304
+ }, node_buffer_1.Buffer.from(`\
305
+ [*]
306
+ insert_final_newline = TRUE
307
+ trim_trailing_whitespace = False
308
+ indent_style = SPACE
309
+ charset = NULL
310
+ `));
311
+ (0, chai_1.expect)(matcher(path.join(__dirname, 'index.js'))).to.eql(Object.assign(Object.create(null), {
312
+ insert_final_newline: true,
313
+ trim_trailing_whitespace: false,
314
+ indent_style: 'space',
315
+ charset: 'null',
316
+ }));
207
317
  });
208
318
  });
209
319
  describe('unset', () => {
210
- it('pair witht the value `unset`', () => {
320
+ it('pair with the value `unset`', () => {
211
321
  const matcher = editorconfig.matcher({
212
322
  root: __dirname,
213
323
  unset: true,
@@ -218,7 +328,7 @@ indent_size = 4
218
328
  [*.json]
219
329
  indent_size = unset
220
330
  `));
221
- matcher(path.join(__dirname, 'index.js')).should.include({ indent_size: 4 });
222
- matcher(path.join(__dirname, 'index.json')).should.be.eql({});
331
+ (0, chai_1.expect)(matcher(path.join(__dirname, 'index.js'))).to.include({ indent_size: 4 });
332
+ (0, chai_1.expect)(matcher(path.join(__dirname, 'index.json'))).to.be.eql(Object.create(null));
223
333
  });
224
334
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editorconfig",
3
- "version": "3.0.2",
3
+ "version": "4.0.1",
4
4
  "description": "EditorConfig File Locator and Interpreter for Node.js",
5
5
  "keywords": [
6
6
  "editorconfig",
@@ -17,7 +17,12 @@
17
17
  "Trey Hunner (http://treyhunner.com)",
18
18
  "Joe Hildebrand (https://github.com/hildjj/)",
19
19
  "SunsetTechuila (https://github.com/SunsetTechuila/)",
20
- "Rex Lorenzo (https://github.com/rlorenzo/)"
20
+ "Rex Lorenzo (https://github.com/rlorenzo/)",
21
+ "Gareth Jones (https://github.com/G-Rath)",
22
+ "Lorie (https://github.com/Lorie-Capella)",
23
+ "simpleqt (https://github.com/simpleqt)",
24
+ "Eli (https://github.com/lishaduck)",
25
+ "bardrop (https://github.com/bardrop)"
21
26
  ],
22
27
  "directories": {
23
28
  "bin": "./bin",
@@ -31,12 +36,13 @@
31
36
  "author": "EditorConfig Team",
32
37
  "license": "MIT",
33
38
  "dependencies": {
34
- "@one-ini/wasm": "0.2.1",
35
- "commander": "^14.0.3",
36
- "minimatch": "~10.2.4",
37
- "semver": "^7.7.4"
39
+ "@one-ini/wasm": "0.2.3",
40
+ "commander": "^15.1.0",
41
+ "minimatch": "~11.0.5",
42
+ "semver": "^7.8.6"
38
43
  },
44
+ "packageManager": "pnpm@12.18.0",
39
45
  "engines": {
40
- "node": ">=20"
46
+ "node": ">=22"
41
47
  }
42
48
  }