Skip to content

Commit

Permalink
Add warning severity support
Browse files Browse the repository at this point in the history
  • Loading branch information
illright committed Aug 12, 2024
1 parent 829859e commit 1834e3a
Show file tree
Hide file tree
Showing 9 changed files with 55 additions and 12 deletions.
7 changes: 7 additions & 0 deletions .changeset/quick-news-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@steiger/pretty-reporter': minor
'steiger': minor
'@steiger/types': minor
---

Added "warning" severity to diagnostics and support in Steiger
6 changes: 6 additions & 0 deletions packages/pretty-reporter/example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,43 @@ reportPretty(
},
],
location: { path: '/home/user/project/src/entities' },
severity: 'error',
getRuleDescriptionUrl,
},
{
message: 'This slice has no references. Consider removing it.',
ruleName: 'insignificant-slice',
location: { path: '/home/user/project/src/entities/users' },
severity: 'error',
getRuleDescriptionUrl,
},
{
message: 'This slice has no references. Consider removing it.',
ruleName: 'insignificant-slice',
location: { path: '/home/user/project/src/entities/user' },
severity: 'error',
getRuleDescriptionUrl,
},
{
message:
'Having a folder with the name "api" inside a segment could be confusing because that name is commonly used for segments. Consider renaming it.',
ruleName: 'no-reserved-folder-names',
location: { path: '/home/user/project/src/entities/user/ui/api' },
severity: 'warn',
getRuleDescriptionUrl,
},
{
message: 'This slice has no segments. Consider dividing the code inside into segments.',
ruleName: 'no-segmentless-slices',
location: { path: '/home/user/project/src/pages/home' },
severity: 'error',
getRuleDescriptionUrl,
},
{
message: 'Layer "processes" is deprecated, avoid using it',
ruleName: 'no-processes',
location: { path: '/home/user/project/src/processes' },
severity: 'error',
getRuleDescriptionUrl,
},
],
Expand Down
2 changes: 1 addition & 1 deletion packages/pretty-reporter/src/format-single-diagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import chalk from 'chalk'
import type { AugmentedDiagnostic } from './types.js'

export function formatSingleDiagnostic(d: AugmentedDiagnostic, cwd: string): string {
const x = chalk.red(figures.cross)
const x = d.severity === 'error' ? chalk.red(figures.cross) : chalk.yellow(figures.warning)
const s = chalk.reset(figures.lineDownRight)
const bar = chalk.reset(figures.lineVertical)
const e = chalk.reset(figures.lineUpRight)
Expand Down
12 changes: 11 additions & 1 deletion packages/pretty-reporter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ export function formatPretty(diagnostics: Array<AugmentedDiagnostic>, cwd: strin
return chalk.green(`${figures.tick} No problems found!`)
}

let footer = chalk.red.bold(`Found ${diagnostics.length} problem${s(diagnostics.length)}`)
const errors = diagnostics.filter((d) => d.severity === 'error')
const warnings = diagnostics.filter((d) => d.severity === 'warn')

let footer =
'Found ' +
[
errors.length > 0 && chalk.red.bold(`${errors.length} error${s(errors.length)}`),
warnings.length > 0 && chalk.yellow.bold(`${warnings.length} warning${s(warnings.length)}`),
]
.filter(Boolean)
.join(' and ')

const autofixable = diagnostics.filter((d) => (d.fixes?.length ?? 0) > 0)
if (autofixable.length === diagnostics.length) {
Expand Down
3 changes: 2 additions & 1 deletion packages/pretty-reporter/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Diagnostic } from '@steiger/types'
import type { Diagnostic, Severity } from '@steiger/types'

export interface AugmentedDiagnostic extends Diagnostic {
ruleName: string
severity: Exclude<Severity, 'off'>
getRuleDescriptionUrl(ruleName: string): URL
}
22 changes: 17 additions & 5 deletions packages/steiger/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createEffect, sample, combine } from 'effector'
import { debounce, not } from 'patronum'
import type { Rule, Folder } from '@steiger/types'
import type { Rule, Folder, Severity } from '@steiger/types'
import type { AugmentedDiagnostic } from '@steiger/pretty-reporter'

import { scan, createWatcher } from './features/transfer-fs-to-vfs'
Expand All @@ -12,6 +12,7 @@ function getRuleDescriptionUrl(ruleName: string) {
}

type Config = typeof $config
type SeverityMap = Record<string, Exclude<Severity, 'off'>>

const $enabledRules = combine($config, $rules, (config, rules) => {
const ruleConfigs = config?.rules
Expand All @@ -25,19 +26,30 @@ const $enabledRules = combine($config, $rules, (config, rules) => {
)
})

async function runRules({ vfs, rules }: { vfs: Folder; rules: Array<Rule> }) {
const $severities = $config.map(
(config) =>
Object.fromEntries(Object.entries(config?.rules ?? {}).filter(([, severity]) => severity !== 'off')) as SeverityMap,
)

async function runRules({ vfs, rules, severities }: { vfs: Folder; rules: Array<Rule>; severities: SeverityMap }) {
const ruleResults = await Promise.all(
rules.map((rule) =>
Promise.resolve(rule.check(vfs, { sourceFileExtension: 'js' })).then(({ diagnostics }) =>
diagnostics.map<AugmentedDiagnostic>((d) => ({ ...d, ruleName: rule.name, getRuleDescriptionUrl })),
diagnostics.map<AugmentedDiagnostic>((d) => ({
...d,
ruleName: rule.name,
getRuleDescriptionUrl,
severity: severities[rule.name],
})),
),
),
)
return ruleResults.flat()
}

export const linter = {
run: (path: string) => scan(path).then((vfs) => runRules({ vfs, rules: $enabledRules.getState() })),
run: (path: string) =>
scan(path).then((vfs) => runRules({ vfs, rules: $enabledRules.getState(), severities: $severities.getState() })),
watch: async (path: string) => {
const { vfs, watcher } = await createWatcher(path)

Expand All @@ -46,7 +58,7 @@ export const linter = {

sample({
clock: defer({ clock: [treeChanged, $enabledRules], until: not(runRulesFx.pending) }),
source: { vfs: vfs.$tree, rules: $enabledRules },
source: { vfs: vfs.$tree, rules: $enabledRules, severities: $severities },
target: runRulesFx,
})

Expand Down
10 changes: 9 additions & 1 deletion packages/steiger/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const yargsProgram = yargs(hideBin(process.argv))
describe: 'apply auto-fixes',
type: 'boolean',
})
.option('fail-on-warnings', {
demandOption: false,
describe: 'exit with an error code if there are warnings',
type: 'boolean',
})
.string('_')
.check((argv) => {
const filePaths = argv._
Expand Down Expand Up @@ -86,6 +91,9 @@ if (consoleArgs.watch) {
}

if (stillRelevantDiagnostics.length > 0) {
process.exit(1)
const onlyWarnings = stillRelevantDiagnostics.every((d) => d.severity === 'warn')
if (consoleArgs['fail-on-warnings'] || !onlyWarnings) {
process.exit(1)
}
}
}
2 changes: 1 addition & 1 deletion packages/steiger/src/models/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function buildValidationScheme(rules: Array<Rule>) {

return z.object({
// zod.record requires at least one element in the array, so we need "as [string, ...string[]]"
rules: z.record(z.enum(ruleNames as [string, ...string[]]), z.enum(['off', 'error'])).refine(
rules: z.record(z.enum(ruleNames as [string, ...string[]]), z.enum(['off', 'error', 'warn'])).refine(
(value) => {
const ruleNames = Object.keys(value)
const offRules = ruleNames.filter((name) => value[name] === 'off')
Expand Down
3 changes: 1 addition & 2 deletions packages/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ export type Fix =

export type Config = Array<ConfigObject | Plugin>

// TODO: 'warn' is not supported yet, add it when it is
export type Severity = 'off' | 'error'
export type Severity = 'off' | 'warn' | 'error'

export interface ConfigObject {
/** Globs of files to check */
Expand Down

0 comments on commit 1834e3a

Please sign in to comment.