From f45f25f950b3a3d8b36b445b4bb56dbf53d2b735 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:46:30 -0400 Subject: [PATCH 1/2] Add editor config utils --- lib/utils/editorconfig.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/utils/editorconfig.js diff --git a/lib/utils/editorconfig.js b/lib/utils/editorconfig.js new file mode 100644 index 0000000000..e69de29bb2 From 053f6a31f0f10d1afc170844625331c96042e80d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:36:46 -0400 Subject: [PATCH 2/2] Editorconfig --- lib/utils/editorconfig.js | 17 +++++ package.json | 1 + pnpm-lock.yaml | 55 ++++++++++++++ tests/lib/utils/editorconfig-test.js | 110 +++++++++++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 tests/lib/utils/editorconfig-test.js diff --git a/lib/utils/editorconfig.js b/lib/utils/editorconfig.js index e69de29bb2..9cc869d8b2 100644 --- a/lib/utils/editorconfig.js +++ b/lib/utils/editorconfig.js @@ -0,0 +1,17 @@ +'use strict'; + +const editorconfig = require('editorconfig'); + +/** + * Resolve editorconfig properties for a given file path using the official + * editorconfig library. + * + * Returns an object like `{ indent_size: 4, indent_style: 'space', ... }` + * with only the properties that matched. Returns an empty object if no + * .editorconfig is found or no sections match. + */ +function resolveEditorConfig(filePath) { + return editorconfig.parseSync(filePath); +} + +module.exports = { resolveEditorConfig }; diff --git a/package.json b/package.json index 131c32178b..9ff15cca5c 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "dependencies": { "@ember-data/rfc395-data": "^0.0.4", "css-tree": "^3.0.1", + "editorconfig": "^3.0.2", "ember-eslint-parser": "^0.6.0", "ember-rfc176-data": "^0.3.18", "eslint-utils": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aa1bc87fd..c5e5708e93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: css-tree: specifier: ^3.0.1 version: 3.0.1 + editorconfig: + specifier: ^3.0.2 + version: 3.0.2 ember-eslint-parser: specifier: ^0.6.0 version: 0.6.0(@babel/core@7.28.5)(@typescript-eslint/parser@8.11.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) @@ -843,6 +846,9 @@ packages: '@one-ini/wasm@0.2.0': resolution: {integrity: sha512-n+L/BvrwKUn7q5O3wHGo+CJZAqfewh38+37sk+eBzv/39lM9pPgPRd4sOZRvSRzo0ukLxzyXso4WlGj2oKZ5hA==} + '@one-ini/wasm@0.2.1': + resolution: {integrity: sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1306,6 +1312,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.8.20: resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==} hasBin: true @@ -1319,6 +1329,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1471,6 +1485,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1647,6 +1665,11 @@ packages: engines: {node: '>=18'} hasBin: true + editorconfig@3.0.2: + resolution: {integrity: sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==} + engines: {node: '>=20'} + hasBin: true + electron-to-chromium@1.5.240: resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==} @@ -2727,6 +2750,10 @@ packages: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3269,6 +3296,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4558,6 +4590,8 @@ snapshots: '@one-ini/wasm@0.2.0': {} + '@one-ini/wasm@0.2.1': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5042,6 +5076,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + baseline-browser-mapping@2.8.20: {} before-after-hook@3.0.2: {} @@ -5055,6 +5091,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -5222,6 +5262,8 @@ snapshots: commander@13.1.0: {} + commander@14.0.3: {} + concat-map@0.0.1: {} config-chain@1.1.13: @@ -5388,6 +5430,13 @@ snapshots: minimatch: 10.0.1 semver: 7.7.3 + editorconfig@3.0.2: + dependencies: + '@one-ini/wasm': 0.2.1 + commander: 14.0.3 + minimatch: 10.2.4 + semver: 7.7.4 + electron-to-chromium@1.5.240: {} electron-to-chromium@1.5.42: {} @@ -6692,6 +6741,10 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -7274,6 +7327,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 diff --git a/tests/lib/utils/editorconfig-test.js b/tests/lib/utils/editorconfig-test.js new file mode 100644 index 0000000000..76f9bbfc7e --- /dev/null +++ b/tests/lib/utils/editorconfig-test.js @@ -0,0 +1,110 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { resolveEditorConfig } = require('../../../lib/utils/editorconfig'); + +describe('resolveEditorConfig', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'editorconfig-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('returns empty object when no .editorconfig exists', () => { + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result).toEqual({}); + }); + + it('reads indent_size from .editorconfig', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*]', 'indent_size = 4'].join('\n') + ); + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_size).toBe(4); + }); + + it('matches *.hbs sections', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*.hbs]', 'indent_size = 3'].join('\n') + ); + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_size).toBe(3); + }); + + it('does not match non-matching glob', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*.js]', 'indent_size = 4'].join('\n') + ); + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_size).toBeUndefined(); + }); + + it('handles brace expansion *.{hbs,gjs}', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*.{hbs,gjs}]', 'indent_size = 6'].join('\n') + ); + expect(resolveEditorConfig(path.join(tmpDir, 'test.hbs')).indent_size).toBe(6); + expect(resolveEditorConfig(path.join(tmpDir, 'test.gjs')).indent_size).toBe(6); + expect(resolveEditorConfig(path.join(tmpDir, 'test.js')).indent_size).toBeUndefined(); + }); + + it('later sections override earlier ones', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*]', 'indent_size = 2', '', '[*.hbs]', 'indent_size = 4'].join('\n') + ); + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_size).toBe(4); + }); + + it('inner .editorconfig overrides outer', () => { + // outer + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*]', 'indent_size = 2'].join('\n') + ); + // inner dir + const innerDir = path.join(tmpDir, 'app'); + fs.mkdirSync(innerDir); + fs.writeFileSync(path.join(innerDir, '.editorconfig'), ['[*]', 'indent_size = 4'].join('\n')); + const filePath = path.join(innerDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_size).toBe(4); + }); + + it('sets indent_size to tab when indent_style is tab and indent_size unset', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '', '[*]', 'indent_style = tab'].join('\n') + ); + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_style).toBe('tab'); + expect(result.indent_size).toBe('tab'); + }); + + it('ignores comments', () => { + fs.writeFileSync( + path.join(tmpDir, '.editorconfig'), + ['root = true', '# a comment', '; another comment', '[*]', 'indent_size = 5'].join('\n') + ); + const filePath = path.join(tmpDir, 'test.hbs'); + const result = resolveEditorConfig(filePath); + expect(result.indent_size).toBe(5); + }); +});