Skip to content

Commit b84ebb5

Browse files
committed
Merge branch 'next' of github.com:feature-sliced/steiger into next
2 parents ee3e9c8 + 7f7bbbc commit b84ebb5

File tree

4 files changed

+226
-29
lines changed

4 files changed

+226
-29
lines changed

.changeset/rude-items-shout.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@feature-sliced/steiger-plugin': minor
3+
---
4+
5+
Extend the functionality of `inconsistent-naming` to support multi-word names and handle rename collisions
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/** Extracts individual words in every naming scheme. */
2+
export const wordPattern = /([A-Z0-9]{2,}(?![A-Z][a-z])|[A-Z]?[a-z0-9]+)/g
3+
4+
/**
5+
* Extract the main subject in a multi-word subject name.
6+
*
7+
* @example
8+
* getMainSubject("a book with pages") // "book"
9+
* getMainSubject("admin-users") // "users"
10+
* getMainSubject("receiptsByOrder") // "receipts"
11+
*/
12+
export function getMainSubject(name: string) {
13+
const words = [...name.matchAll(wordPattern)]
14+
.map((match) => match[0])
15+
.filter((word) => !articles.includes(word.toLocaleLowerCase()))
16+
17+
const prepositionIndex = words.findIndex((word) => prepositions.includes(word.toLocaleLowerCase()))
18+
if (prepositionIndex === -1) {
19+
return words[words.length - 1]
20+
}
21+
const mainPart = words.slice(0, prepositionIndex)
22+
return mainPart[mainPart.length - 1]
23+
}
24+
25+
if (import.meta.vitest) {
26+
const { test, expect } = import.meta.vitest
27+
28+
test('getMainSubject', () => {
29+
expect(getMainSubject('a book with pages')).toEqual('book')
30+
expect(getMainSubject('admin-users')).toEqual('users')
31+
expect(getMainSubject('receiptsByOrder')).toEqual('receipts')
32+
})
33+
}
34+
35+
const articles = ['a', 'an', 'the']
36+
const prepositions = [
37+
'about',
38+
'above',
39+
'across',
40+
'after',
41+
'against',
42+
'along',
43+
'among',
44+
'around',
45+
'at',
46+
'before',
47+
'behind',
48+
'below',
49+
'beneath',
50+
'beside',
51+
'besides',
52+
'between',
53+
'beyond',
54+
'but',
55+
'by',
56+
'concerning',
57+
'despite',
58+
'down',
59+
'during',
60+
'except',
61+
'excepting',
62+
'for',
63+
'from',
64+
'in',
65+
'inside',
66+
'into',
67+
'like',
68+
'near',
69+
'of',
70+
'off',
71+
'on',
72+
'onto',
73+
'out',
74+
'outside',
75+
'over',
76+
'past',
77+
'regarding',
78+
'since',
79+
'through',
80+
'throughout',
81+
'to',
82+
'toward',
83+
'under',
84+
'underneath',
85+
'until',
86+
'up',
87+
'upon',
88+
'with',
89+
'within',
90+
'without',
91+
]

packages/steiger-plugin-fsd/src/inconsistent-naming/index.spec.ts

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { expect, it } from 'vitest'
22

3-
import { compareMessages, joinFromRoot, parseIntoFolder as parseIntoFsdRoot } from '@steiger/toolkit'
3+
import { joinFromRoot, parseIntoFolder as parseIntoFsdRoot } from '@steiger/toolkit'
44
import inconsistentNaming from './index.js'
55

6-
it('reports no errors on slice names that are pluralized consistently', () => {
7-
const root = parseIntoFsdRoot(
6+
it('reports no errors on entity names that are pluralized consistently', () => {
7+
const root1 = parseIntoFsdRoot(
88
`
99
📂 entities
1010
📂 users
@@ -16,11 +16,41 @@ it('reports no errors on slice names that are pluralized consistently', () => {
1616
`,
1717
joinFromRoot('users', 'user', 'project', 'src'),
1818
)
19+
const root2 = parseIntoFsdRoot(
20+
`
21+
📂 entities
22+
📂 user
23+
📂 ui
24+
📄 index.ts
25+
📂 post
26+
📂 ui
27+
📄 index.ts
28+
`,
29+
joinFromRoot('users', 'user', 'project', 'src'),
30+
)
31+
32+
expect(inconsistentNaming.check(root1)).toEqual({ diagnostics: [] })
33+
expect(inconsistentNaming.check(root2)).toEqual({ diagnostics: [] })
34+
})
35+
36+
it('reports no errors on multi-word entity names that are pluralized consistently', () => {
37+
const root = parseIntoFsdRoot(
38+
`
39+
📂 entities
40+
📂 admin-users
41+
📂 ui
42+
📄 index.ts
43+
📂 employers-of-record
44+
📂 ui
45+
📄 index.ts
46+
`,
47+
joinFromRoot('users', 'user', 'project', 'src'),
48+
)
1949

2050
expect(inconsistentNaming.check(root)).toEqual({ diagnostics: [] })
2151
})
2252

23-
it('reports an error on slice names that are not pluralized consistently', () => {
53+
it('reports an error on entity names that are not pluralized consistently', () => {
2454
const root = parseIntoFsdRoot(
2555
`
2656
📂 entities
@@ -34,15 +64,15 @@ it('reports an error on slice names that are not pluralized consistently', () =>
3464
joinFromRoot('users', 'user', 'project', 'src'),
3565
)
3666

37-
const diagnostics = inconsistentNaming.check(root).diagnostics.sort(compareMessages)
67+
const diagnostics = inconsistentNaming.check(root).diagnostics
3868
expect(diagnostics).toEqual([
3969
{
40-
message: 'Inconsistent pluralization of slice names. Prefer all plural names',
70+
message: 'Inconsistent pluralization of entity names. Prefer all singular names.',
4171
fixes: [
4272
{
4373
type: 'rename',
44-
path: joinFromRoot('users', 'user', 'project', 'src', 'entities', 'user'),
45-
newName: 'users',
74+
path: joinFromRoot('users', 'user', 'project', 'src', 'entities', 'posts'),
75+
newName: 'post',
4676
},
4777
],
4878
location: { path: joinFromRoot('users', 'user', 'project', 'src', 'entities') },
@@ -54,10 +84,10 @@ it('prefers the singular form when there are more singular slices', () => {
5484
const root = parseIntoFsdRoot(
5585
`
5686
📂 entities
57-
📂 user
87+
📂 admin-user
5888
📂 ui
5989
📄 index.ts
60-
📂 post
90+
📂 news-post
6191
📂 ui
6292
📄 index.ts
6393
📂 comments
@@ -67,10 +97,10 @@ it('prefers the singular form when there are more singular slices', () => {
6797
joinFromRoot('users', 'user', 'project', 'src'),
6898
)
6999

70-
const diagnostics = inconsistentNaming.check(root).diagnostics.sort(compareMessages)
100+
const diagnostics = inconsistentNaming.check(root).diagnostics
71101
expect(diagnostics).toEqual([
72102
{
73-
message: 'Inconsistent pluralization of slice names. Prefer all singular names',
103+
message: 'Inconsistent pluralization of entity names. Prefer all singular names.',
74104
fixes: [
75105
{
76106
type: 'rename',
@@ -82,3 +112,48 @@ it('prefers the singular form when there are more singular slices', () => {
82112
},
83113
])
84114
})
115+
116+
it('recognizes the special case when there is a plural and singular form of the same name', () => {
117+
const root = parseIntoFsdRoot(
118+
`
119+
📂 entities
120+
📂 admin-user
121+
📂 ui
122+
📄 index.ts
123+
📂 admin-users
124+
📂 ui
125+
📄 index.ts
126+
`,
127+
joinFromRoot('users', 'user', 'project', 'src'),
128+
)
129+
130+
const diagnostics = inconsistentNaming.check(root).diagnostics
131+
expect(diagnostics).toEqual([
132+
{
133+
message: 'Avoid having both "admin-user" and "admin-users" entities.',
134+
location: { path: joinFromRoot('users', 'user', 'project', 'src', 'entities', 'admin-user') },
135+
},
136+
])
137+
})
138+
139+
it('allows inconsistency between different slice groups', () => {
140+
const root = parseIntoFsdRoot(
141+
`
142+
📂 entities
143+
📂 admin-user
144+
📂 ui
145+
📄 index.ts
146+
📂 group
147+
📄 index.ts
148+
📂 post-parts
149+
📂 posts
150+
📄 index.ts
151+
📂 authors
152+
📄 index.ts
153+
`,
154+
joinFromRoot('users', 'user', 'project', 'src'),
155+
)
156+
157+
const diagnostics = inconsistentNaming.check(root).diagnostics
158+
expect(diagnostics).toEqual([])
159+
})

packages/steiger-plugin-fsd/src/inconsistent-naming/index.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import type { PartialDiagnostic, Rule } from '@steiger/toolkit'
77

88
import { groupSlices } from '../_lib/group-slices.js'
99
import { NAMESPACE } from '../constants.js'
10+
import { getMainSubject } from './get-main-subject.js'
1011

11-
/** Detect inconsistent naming of slices on layers (singular vs plural) */
12+
/** Detect inconsistent pluralization of entities. */
1213
const inconsistentNaming = {
1314
name: `${NAMESPACE}/inconsistent-naming` as const,
1415
check(root) {
@@ -22,29 +23,54 @@ const inconsistentNaming = {
2223
const slices = getSlices(entities)
2324
const sliceNames = groupSlices(Object.keys(slices))
2425
for (const [groupPrefix, group] of Object.entries(sliceNames)) {
25-
const [pluralNames, singularNames] = partition(group, isPlural)
26+
const [pluralNames, singularNames] = partition(
27+
group.map((name) => [name, getMainSubject(name)] as const),
28+
([, mainSubject]) => isPlural(mainSubject),
29+
)
30+
31+
/** Names that exist in both singular and plural forms for filtering later. */
32+
const duplicates = {
33+
singular: [] as Array<string>,
34+
plural: [] as Array<string>,
35+
}
2636

2737
if (pluralNames.length > 0 && singularNames.length > 0) {
28-
const message = 'Inconsistent pluralization of slice names'
38+
for (const [singularName, mainSubject] of singularNames) {
39+
const pluralized = singularName.replace(mainSubject, plural(mainSubject))
40+
if (group.includes(pluralized)) {
41+
duplicates.singular.push(singularName)
42+
duplicates.plural.push(pluralized)
43+
44+
diagnostics.push({
45+
message: `Avoid having both "${singularName}" and "${plural(singularName)}" entities${groupPrefix === '' ? '' : ' in the same slice group'}.`,
46+
location: { path: join(entities.path, groupPrefix, singularName) },
47+
})
48+
}
49+
}
2950

30-
if (pluralNames.length >= singularNames.length) {
51+
const message = 'Inconsistent pluralization of entity names'
52+
if (pluralNames.length > singularNames.length && singularNames.length > duplicates.singular.length) {
3153
diagnostics.push({
32-
message: `${message}. Prefer all plural names`,
33-
fixes: singularNames.map((name) => ({
34-
type: 'rename',
35-
path: join(entities.path, groupPrefix, name),
36-
newName: plural(name),
37-
})),
54+
message: `${message}. Prefer all plural names.`,
55+
fixes: singularNames
56+
.filter(([name]) => !duplicates.singular.includes(name))
57+
.map(([name, mainWord]) => ({
58+
type: 'rename',
59+
path: join(entities.path, groupPrefix, name),
60+
newName: name.replace(mainWord, plural(mainWord)),
61+
})),
3862
location: { path: join(entities.path, groupPrefix) },
3963
})
40-
} else {
64+
} else if (pluralNames.length > duplicates.plural.length) {
4165
diagnostics.push({
42-
message: `${message}. Prefer all singular names`,
43-
fixes: pluralNames.map((name) => ({
44-
type: 'rename',
45-
path: join(entities.path, groupPrefix, name),
46-
newName: singular(name),
47-
})),
66+
message: `${message}. Prefer all singular names.`,
67+
fixes: pluralNames
68+
.filter(([name]) => !duplicates.plural.includes(name))
69+
.map(([name, mainWord]) => ({
70+
type: 'rename',
71+
path: join(entities.path, groupPrefix, name),
72+
newName: name.replace(mainWord, singular(mainWord)),
73+
})),
4874
location: { path: join(entities.path, groupPrefix) },
4975
})
5076
}

0 commit comments

Comments
 (0)