eslint-plugin-smarthr 6.10.0 → 6.10.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [6.10.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.10.0...eslint-plugin-smarthr-v6.10.1) (2026-04-08)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **require-barrel-import:** next.jsの特殊文字パスでのバグ修正とリファクタリング ([#1210](https://github.com/kufu/tamatebako/issues/1210)) ([458cfe9](https://github.com/kufu/tamatebako/commit/458cfe90084cbcee936bcb375216f5807880edd7))
11
+
5
12
  ## [6.10.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.9.1...eslint-plugin-smarthr-v6.10.0) (2026-04-08)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.10.0",
3
+ "version": "6.10.1",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "9f682d10a15702c061e23d7c0922e0d3f69149d5"
40
+ "gitHead": "e2e55eea3e72809fab55082ad6ef4da0c6025ae8"
41
41
  }
@@ -31,62 +31,169 @@ const SCHEMA = [
31
31
  }
32
32
  ]
33
33
 
34
- const entriedReplacePaths = Object.entries(replacePaths)
35
34
  const CWD = process.cwd()
36
35
  const REGEX_UNNECESSARY_SLASH = /(\/)+/g
37
36
  const REGEX_ROOT_PATH = new RegExp(`^${rootPath}/index\.`)
38
37
  const REGEX_INDEX_FILE = /\/index\.(ts|js)x?$/
39
38
  const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
40
39
 
41
- // 正規表現を事前生成してキャッシュ
42
- const entriedReplacePathsWithRegex = entriedReplacePaths.map(([key, values]) => [
40
+ // Path aliasの正規表現を事前生成してキャッシュ
41
+ const entriedReplacePathsWithRegex = Object.entries(replacePaths).map(([key, values]) => [
43
42
  key,
44
43
  values,
45
44
  new RegExp(`^${key}(.+)$`),
46
45
  values.map(v => new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`))
47
46
  ])
48
47
 
49
- const calculateAbsoluteImportPath = (source) => {
50
- if (source[0] === '/') {
51
- return source
48
+ /**
49
+ * Path aliasを絶対パスに変換する
50
+ * @param {string} importPath - import文のパス(例: '@/components/Button')
51
+ * @returns {string} 絶対パス(例: '/path/to/src/components/Button')
52
+ */
53
+ const resolvePathAlias = (importPath) => {
54
+ if (importPath[0] === '/') {
55
+ return importPath
52
56
  }
53
57
 
54
- return entriedReplacePathsWithRegex.reduce((prev, [key, values, keyRegex]) => {
55
- if (source === prev) {
56
- return values.reduce((p, v) => {
57
- if (prev === p && keyRegex.test(keyRegex)) {
58
- return p.replace(keyRegex, `${path.resolve(`${CWD}/${v}`)}/$1`)
58
+ return entriedReplacePathsWithRegex.reduce((result, [key, values, keyRegex]) => {
59
+ if (result === importPath) {
60
+ return values.reduce((resolved, value) => {
61
+ if (resolved === result && keyRegex.test(result)) {
62
+ return resolved.replace(keyRegex, `${path.resolve(`${CWD}/${value}`)}/$1`)
59
63
  }
60
-
61
- return p
62
- }, prev)
64
+ return resolved
65
+ }, result)
63
66
  }
64
-
65
- return prev
66
- }, source)
67
+ return result
68
+ }, importPath)
67
69
  }
68
- const calculateReplacedImportPath = (source) => {
69
- return entriedReplacePathsWithRegex.reduce((prev, [key, values, keyRegex, valueRegexes]) => {
70
- if (source === prev) {
71
- return values.reduce((p, v, index) => {
72
- if (prev === p) {
73
- const regexp = valueRegexes[index]
74
70
 
75
- if (regexp.test(prev)) {
76
- return p.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
71
+ /**
72
+ * 絶対パスをPath aliasに変換する
73
+ * @param {string} absolutePath - 絶対パス(例: '/path/to/src/components/Button')
74
+ * @returns {string} Path alias(例: '@/components/Button')
75
+ */
76
+ const convertToPathAlias = (absolutePath) => {
77
+ return entriedReplacePathsWithRegex.reduce((result, [key, values, keyRegex, valueRegexes]) => {
78
+ if (result === absolutePath) {
79
+ return values.reduce((converted, value, index) => {
80
+ if (converted === result) {
81
+ const regexp = valueRegexes[index]
82
+ if (regexp.test(converted)) {
83
+ return converted.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
77
84
  }
78
85
  }
86
+ return converted
87
+ }, result)
88
+ }
89
+ return result
90
+ }, absolutePath)
91
+ }
92
+
93
+ /**
94
+ * import先がimport元の内部にあるかチェック(同階層・サブディレクトリからのimport)
95
+ * (Next.js App Routerの特殊文字パスにも対応)
96
+ * @param {string} importerDir - import元のディレクトリ
97
+ * @param {string} importedPath - import先のパス
98
+ * @returns {boolean}
99
+ */
100
+ const isImportedInsideImporter = (importerDir, importedPath) => {
101
+ return importedPath === importerDir || importedPath.startsWith(importerDir + '/')
102
+ }
79
103
 
80
- return p
81
- }, prev)
104
+ /**
105
+ * allowedImportsオプションに基づいて、特定のimportが許可されているかチェックする
106
+ * @param {object} node - ImportDeclaration node
107
+ * @param {string} importerDir - import元のディレクトリ
108
+ * @param {Array} targetAllowedImports - 適用されるallowedImportsのキー配列
109
+ * @param {object} allowedImportsConfig - allowedImportsの設定
110
+ * @returns {{ shouldSkip: boolean, deniedModules: Array }} チェック結果
111
+ */
112
+ const checkAllowedImports = (node, importerDir, targetAllowedImports, allowedImportsConfig) => {
113
+ let isDenyPath = false
114
+ let deniedModules = []
115
+
116
+ for (const allowedKey of targetAllowedImports) {
117
+ const allowedOption = allowedImportsConfig[allowedKey]
118
+
119
+ for (const targetModule in allowedOption) {
120
+ const actualTarget = targetModule[0] !== '.'
121
+ ? targetModule
122
+ : path.resolve(`${CWD}/${targetModule}`)
123
+
124
+ let importSource = node.source.value
125
+
126
+ // 絶対パスの場合は、import元ディレクトリを基準に解決
127
+ if (actualTarget[0] === '/') {
128
+ importSource = path.resolve(`${importerDir}/${importSource}`)
129
+ }
130
+
131
+ if (actualTarget !== importSource) {
132
+ continue
133
+ }
134
+
135
+ const allowedModules = allowedOption[targetModule] || true
136
+
137
+ if (!Array.isArray(allowedModules)) {
138
+ isDenyPath = true
139
+ deniedModules.push(true)
140
+ } else {
141
+ const importedNames = node.specifiers.map(s => s.imported?.name)
142
+ const notAllowedModules = importedNames.filter(name => !allowedModules.includes(name))
143
+ deniedModules.push(notAllowedModules)
144
+ }
82
145
  }
146
+ }
147
+
148
+ // 完全に許可されている場合はスキップ
149
+ const shouldSkip =
150
+ (isDenyPath && deniedModules[0] === true) ||
151
+ (!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0)
83
152
 
84
- return prev
85
- }, source)
153
+ return { shouldSkip, deniedModules }
86
154
  }
87
155
 
88
- const pickImportedName = (s) => s.imported?.name
89
- const findExistsSync = (p) => fs.existsSync(p)
156
+ /**
157
+ * import先のパスから親方向に barrel ファイルを探索する
158
+ * @param {string} importedPath - import先の絶対パス
159
+ * @param {string} importerDir - import元のディレクトリ
160
+ * @returns {string|undefined} 見つかったbarrelファイルのパス
161
+ */
162
+ const findBarrelFile = (importedPath, importerDir) => {
163
+ const pathSegments = importedPath.split('/')
164
+ let currentPath = importedPath
165
+ let barrel = undefined
166
+
167
+ // ディレクトリ指定の場合、そのindex.tsを指していることは自明なので一階層上から探索
168
+ if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
169
+ pathSegments.pop()
170
+ currentPath = pathSegments.join('/')
171
+ }
172
+
173
+ while (pathSegments.length > 0) {
174
+ // 以下の場合は探索終了
175
+ // 1. root pathに到達した場合
176
+ // 2. import先がimport元の内部にある場合(同階層・サブディレクトリからのimport)
177
+ if (importerDir === rootPath || isImportedInsideImporter(importerDir, currentPath)) {
178
+ break
179
+ }
180
+
181
+ // 現在のパスにbarrelファイルがあるかチェック
182
+ const foundBarrel = TARGET_EXTS
183
+ .map(ext => `${currentPath}/index.${ext}`)
184
+ .find(filePath => fs.existsSync(filePath))
185
+
186
+ if (foundBarrel) {
187
+ barrel = foundBarrel
188
+ }
189
+
190
+ // 一階層上に移動
191
+ pathSegments.pop()
192
+ currentPath = pathSegments.join('/')
193
+ }
194
+
195
+ return barrel
196
+ }
90
197
 
91
198
  /**
92
199
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
@@ -99,108 +206,78 @@ module.exports = {
99
206
  create(context) {
100
207
  const option = context.options[0] || {}
101
208
 
102
- if (option.ignores && option.ignores.some((i) => (new RegExp(i)).test(context.filename))) {
103
- return {}
209
+ // ignoresオプションでスキップ対象のファイルかチェック
210
+ if (option.ignores) {
211
+ const isIgnored = option.ignores.some(pattern =>
212
+ new RegExp(pattern).test(context.filename)
213
+ )
214
+ if (isIgnored) {
215
+ return {}
216
+ }
104
217
  }
105
218
 
106
- const dir = getParentDir(context.filename)
219
+ const importerDir = getParentDir(context.filename)
220
+
221
+ // このファイルに適用されるallowedImportsのキーを収集
107
222
  const targetAllowedImports = []
108
223
  if (option?.allowedImports) {
109
- for (const regex in option.allowedImports) {
110
- if ((new RegExp(regex)).test(context.filename)) {
111
- targetAllowedImports.push(regex)
224
+ for (const pattern in option.allowedImports) {
225
+ if (new RegExp(pattern).test(context.filename)) {
226
+ targetAllowedImports.push(pattern)
112
227
  }
113
228
  }
114
229
  }
115
230
 
116
231
  return {
117
232
  ImportDeclaration: (node) => {
118
- let isDenyPath = false
119
- let deniedModules = []
120
-
121
- for (const allowedKey of targetAllowedImports) {
122
- const allowedOption = option.allowedImports[allowedKey]
123
-
124
- for (const targetModule in allowedOption) {
125
- const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${CWD}/${targetModule}`)
126
- let sourceValue = node.source.value
127
-
128
- if (actualTarget[0] === '/') {
129
- sourceValue = path.resolve(`${dir}/${sourceValue}`)
130
- }
131
-
132
- if (actualTarget !== sourceValue) {
133
- continue
134
- }
135
-
136
- let allowedModules = allowedOption[targetModule] || true
137
-
138
- if (!Array.isArray(allowedModules)) {
139
- isDenyPath = true
140
- deniedModules.push(true)
141
- } else {
142
- deniedModules.push(node.specifiers.map(pickImportedName).filter(i => allowedModules.indexOf(i) == -1))
143
- }
144
- }
145
- }
146
-
147
- if (
148
- isDenyPath && deniedModules[0] === true ||
149
- !isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0
150
- ) {
233
+ // allowedImportsチェック
234
+ const { shouldSkip, deniedModules } = checkAllowedImports(
235
+ node,
236
+ importerDir,
237
+ targetAllowedImports,
238
+ option.allowedImports || {}
239
+ )
240
+ if (shouldSkip) {
151
241
  return
152
242
  }
153
243
 
154
- let sourceValue = node.source.value
244
+ // import先のパスを絶対パスに変換
245
+ let importedPath = node.source.value
155
246
 
156
- if (sourceValue[0] === '.') {
157
- sourceValue = path.resolve(`${dir}/${sourceValue}`)
247
+ // 相対パスの場合、絶対パスに変換
248
+ if (importedPath[0] === '.') {
249
+ importedPath = path.resolve(`${importerDir}/${importedPath}`)
158
250
  }
159
251
 
160
- sourceValue = calculateAbsoluteImportPath(sourceValue)
252
+ // Path alias(@/, ~/など)を絶対パスに変換
253
+ importedPath = resolvePathAlias(importedPath)
161
254
 
162
- if (sourceValue[0] !== '/') {
255
+ // 絶対パスでない場合(node_modulesなど)はスキップ
256
+ if (importedPath[0] !== '/') {
163
257
  return
164
258
  }
165
259
 
166
- const sources = sourceValue.split('/')
260
+ // barrel ファイルを探索
261
+ const barrelPath = findBarrelFile(importedPath, importerDir)
167
262
 
168
- // HINT: directoryの場合、indexファイルからimportしていることは自明であるため、一階層上からチェックする
169
- if (fs.existsSync(sourceValue) && fs.statSync(sourceValue).isDirectory()) {
170
- sources.pop()
171
- sourceValue = sources.join('/')
172
- }
173
-
174
- let barrel = undefined
175
-
176
- while (sources.length > 0) {
177
- // HINT: 以下の場合は即終了
178
- // - import元以下のimportだった場合
179
- // - rootまで捜索した場合
180
- if (
181
- dir === rootPath ||
182
- dir.match(new RegExp(`^${sourceValue}`))
183
- ) {
184
- break
185
- }
186
-
187
- barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find(findExistsSync) || barrel
188
-
189
- sources.pop()
190
- sourceValue = sources.join('/')
263
+ // barrel が見つからない、またはroot pathのindex.tsの場合はスキップ
264
+ if (!barrelPath || REGEX_ROOT_PATH.test(barrelPath)) {
265
+ return
191
266
  }
192
267
 
193
- if (barrel && !barrel.match(REGEX_ROOT_PATH)) {
194
- barrel = calculateReplacedImportPath(barrel)
195
- const noExt = barrel.replace(REGEX_INDEX_FILE, '')
196
- deniedModules = [...new Set(deniedModules.flat())]
197
-
198
- context.report({
199
- node,
200
- message: `${deniedModules.length ? `${deniedModules.join(', ')} は ${noExt} からimportしてください` : `${noExt} からimportするか、${barrel} のbarrelファイルを削除して直接import可能にしてください`}
201
- - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import`,
202
- });
203
- }
268
+ // barrel パスをPath aliasに変換
269
+ const barrelWithAlias = convertToPathAlias(barrelPath)
270
+ const barrelDirWithAlias = barrelWithAlias.replace(REGEX_INDEX_FILE, '')
271
+ const uniqueDeniedModules = [...new Set(deniedModules.flat())]
272
+
273
+ // エラーを報告
274
+ context.report({
275
+ node,
276
+ message: uniqueDeniedModules.length
277
+ ? `${uniqueDeniedModules.join(', ')} は ${barrelDirWithAlias} からimportしてください`
278
+ : `${barrelDirWithAlias} からimportするか、${barrelWithAlias} のbarrelファイルを削除して直接import可能にしてください`
279
+ + '\n - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import',
280
+ })
204
281
  },
205
282
  }
206
283
  },