Skip to content

Commit

Permalink
Add the typo-in-layer-name rule (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
Espate authored Sep 26, 2024
1 parent 1bb6c4b commit 7057543
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/unlucky-coins-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@feature-sliced/steiger-plugin': minor
'steiger': minor
---

Add typo-in-layer-name rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Currently, Steiger is not extendable with more rules, though that will change in
<tr> <td><a href="./packages/steiger-plugin-fsd/src/repetitive-naming/README.md"><code>repetitive-naming</code></a></td> <td>Ensure that all entities are named consistently in terms of pluralization.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/segments-by-purpose/README.md"><code>segments-by-purpose</code></a></td> <td>Discourage the use of segment names that group code by its essence, and instead encourage grouping by purpose</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/shared-lib-grouping/README.md"><code>shared-lib-grouping</code></a></td> <td>Forbid having too many ungrouped modules in <code>shared/lib</code>.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/typo-in-layer-name/README.md"><code>typo-in-layer-name</code></a></td> <td>Ensure that all layers are named without any typos.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/no-processes/README.md"><code>no-processes</code></a></td> <td>Discourage the use of the deprecated Processes layer.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/import-locality/README.md"><code>import-locality</code></a></td> <td>Require that imports from the same slice be relative and imports from one slice to another be absolute.</td> </tr>
</tbody>
Expand Down
1 change: 1 addition & 0 deletions packages/steiger-plugin-fsd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
],
"dependencies": {
"@feature-sliced/filesystem": "^2.2.5",
"fastest-levenshtein": "^1.0.16",
"lodash-es": "^4.17.21",
"pluralize": "^8.0.0",
"precinct": "^12.1.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/steiger-plugin-fsd/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import publicApi from './public-api/index.js'
import repetitiveNaming from './repetitive-naming/index.js'
import segmentsByPurpose from './segments-by-purpose/index.js'
import sharedLibGrouping from './shared-lib-grouping/index.js'
import typoInLayerName from './typo-in-layer-name/index.js'
import noProcesses from './no-processes/index.js'
import packageJson from '../package.json'

Expand All @@ -34,6 +35,7 @@ const allRules: Array<Rule> = [
repetitiveNaming,
segmentsByPurpose,
sharedLibGrouping,
typoInLayerName,
noProcesses,
]

Expand Down
29 changes: 29 additions & 0 deletions packages/steiger-plugin-fsd/src/typo-in-layer-name/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# `typo-in-layer-name`

Ensure that all layers are named consistently without any typos.

Examples of project structures that pass this rule:

```
📂 shared
📂 entities
📂 features
📂 widgets
📂 pages
📂 app
```

Examples of project structures that fail this rule:

```
📂 shraed // ❌
📂 entities
📂 fietures // ❌
📂 wigdets // ❌
📂 page // ❌
📂 app
```

## Rationale

The methodology contains a standardized set of layers. Enforcing these naming conventions is important for other developers, as well as for other rules of the linter to work correctly.
100 changes: 100 additions & 0 deletions packages/steiger-plugin-fsd/src/typo-in-layer-name/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { expect, it } from 'vitest'

import typoInLayerName from './index.js'
import { joinFromRoot, parseIntoFsdRoot } from '../_lib/prepare-test.js'

it('reports no errors on a project without typos in layer names', () => {
const root = parseIntoFsdRoot(`
📂 shared
📂 entities
📂 features
📂 widgets
📂 pages
📂 app
`)

expect(typoInLayerName.check(root)).toEqual({ diagnostics: [] })
})

it('reports errors on a project with typos in layer names', () => {
const root = parseIntoFsdRoot(`
📂 shraed
📂 entities
📂 fietures
📂 wigdets
📂 page
📂 app
`)

const diagnostics = typoInLayerName.check(root).diagnostics
expect(diagnostics).toEqual([
{
message: 'Layer "page" potentially contains a typo. Did you mean "pages"?',
location: { path: joinFromRoot('page') },
},
{
message: 'Layer "shraed" potentially contains a typo. Did you mean "shared"?',
location: { path: joinFromRoot('shraed') },
},
{
message: 'Layer "fietures" potentially contains a typo. Did you mean "features"?',
location: { path: joinFromRoot('fietures') },
},
{
message: 'Layer "wigdets" potentially contains a typo. Did you mean "widgets"?',
location: { path: joinFromRoot('wigdets') },
},
])
})

it('reports no errors on a project with custom layers if base layers are present', () => {
const root = parseIntoFsdRoot(`
📂 shared
📂 shapes
📂 entities
📂 entries
📂 features
📂 fixtures
📂 widgets
📂 pages
📂 places
📂 app
📂 amp
`)

expect(typoInLayerName.check(root)).toEqual({ diagnostics: [] })
})

it('reports errors on a project with custom layers if base layers are absent', () => {
const root = parseIntoFsdRoot(`
📂 shapes
📂 entries
📂 fixtures
📂 places
📂 amp
`)

const diagnostics = typoInLayerName.check(root).diagnostics
expect(diagnostics).toEqual([
{
message: 'Layer "amp" potentially contains a typo. Did you mean "app"?',
location: { path: joinFromRoot('amp') },
},
{
message: 'Layer "shapes" potentially contains a typo. Did you mean "shared"?',
location: { path: joinFromRoot('shapes') },
},
{
message: 'Layer "entries" potentially contains a typo. Did you mean "entities"?',
location: { path: joinFromRoot('entries') },
},
{
message: 'Layer "fixtures" potentially contains a typo. Did you mean "features"?',
location: { path: joinFromRoot('fixtures') },
},
{
message: 'Layer "places" potentially contains a typo. Did you mean "pages"?',
location: { path: joinFromRoot('places') },
},
])
})
68 changes: 68 additions & 0 deletions packages/steiger-plugin-fsd/src/typo-in-layer-name/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Diagnostic, Rule } from '@steiger/types'
import { NAMESPACE } from '../constants.js'
import { LayerName, layerSequence } from '@feature-sliced/filesystem'
import { distance } from 'fastest-levenshtein'
import { basename } from 'node:path'
import { joinFromRoot } from '../_lib/prepare-test.js'

const LEVENSHTEIN_DISTANCE_UPPER_BOUND = 3

const typoInLayerName = {
name: `${NAMESPACE}/typo-in-layer-name`,
check(root) {
const diagnostics: Array<Diagnostic> = []

// construct list of suggestions, like [{ input: 'shraed', suggestion: 'shared', distance: 2 }, ...],
// limit Levenshtein distance upper bound to 3,
// sort by Levenshtein distance in ascending order,
const suggestionsList = root.children
.filter((child) => child.type === 'folder')
.flatMap((child) => {
const layer = basename(child.path)

return layerSequence
.map((sequenceLayer) => ({
input: layer,
suggestion: sequenceLayer,
distance: distance(layer, sequenceLayer),
}))
.filter((layer) => layer.distance <= LEVENSHTEIN_DISTANCE_UPPER_BOUND)
})
.sort((a, b) => a.distance - b.distance)

const processedInputs: string[] = []
const suggestedLayers: LayerName[] = []

suggestionsList.forEach((layer) => {
// if Levenshtein distance is 0, the layer name is correct - add it as a "suggestion"
if (layer.distance === 0) {
suggestedLayers.push(layer.suggestion)

return
}

// if the input is already processed, the suggestion for this input is already added
if (processedInputs.includes(layer.input)) {
return
}

// if the suggestion is already added, it cannot be used for this input
if (suggestedLayers.includes(layer.suggestion)) {
return
}

// mark the input as processed & add suitable suggestion
processedInputs.push(layer.input)
suggestedLayers.push(layer.suggestion)

diagnostics.push({
message: `Layer "${layer.input}" potentially contains a typo. Did you mean "${layer.suggestion}"?`,
location: { path: joinFromRoot(layer.input) },
})
})

return { diagnostics }
},
} satisfies Rule

export default typoInLayerName
1 change: 1 addition & 0 deletions packages/steiger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Currently, Steiger is not extendable with more rules, though that will change in
<tr> <td><a href="./packages/steiger-plugin-fsd/src/repetitive-naming/README.md"><code>repetitive-naming</code></a></td> <td>Ensure that all entities are named consistently in terms of pluralization.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/segments-by-purpose/README.md"><code>segments-by-purpose</code></a></td> <td>Discourage the use of segment names that group code by its essence, and instead encourage grouping by purpose</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/shared-lib-grouping/README.md"><code>shared-lib-grouping</code></a></td> <td>Forbid having too many ungrouped modules in <code>shared/lib</code>.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/typo-in-layer-name/README.md"><code>typo-in-layer-name</code></a></td> <td>Ensure that all layers are named without any typos.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/no-processes/README.md"><code>no-processes</code></a></td> <td>Discourage the use of the deprecated Processes layer.</td> </tr>
<tr> <td><a href="./packages/steiger-plugin-fsd/src/import-locality/README.md"><code>import-locality</code></a></td> <td>Require that imports from the same slice be relative and imports from one slice to another be absolute.</td> </tr>
</tbody>
Expand Down
1 change: 1 addition & 0 deletions packages/steiger/migrations/convert-config-to-flat.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const ruleNames = [
'repetitive-naming',
'segments-by-purpose',
'shared-lib-grouping',
'typo-in-layer-name',
'no-processes',
'import-locality',
]
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7057543

Please sign in to comment.