Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ Rules to help prevent errors.
| [no-duplicate-keyframes](src/rules/no-duplicate-keyframes/README.md) | Disallow duplicate `@keyframes` names |
| [no-duplicate-registered-properties](src/rules/no-duplicate-registered-properties/README.md) | Disallow duplicate `@property` registrations |
| [no-invalid-z-index](src/rules/no-invalid-z-index/README.md) | Disallow `z-index` values that are not valid 32-bit integers |
| [no-missing-if-else](src/rules/no-missing-if-else/README.md) | Require every `if()` function to include an `else` condition |
| [no-unreachable-if-branches](src/rules/no-unreachable-if-branches/README.md) | Require the `else` branch of an `if()` function to always be the last branch |
| [no-useless-if](src/rules/no-useless-if/README.md) | Disallow `if()` functions whose only branch is `else` |
| [no-unused-layers](src/rules/no-unused-layers/README.md) | Disallow `@layer` names that are declared but never implemented |
| [block-no-empty](https://stylelint.io/user-guide/rules/block-no-empty) _(stylelint core)_ | Disallow empty rules and at-rules (including those containing only comments) |
| [keyframe-declaration-no-important](https://stylelint.io/user-guide/rules/keyframe-declaration-no-important) _(stylelint core)_ | Disallow `!important` declarations inside `@keyframes` blocks |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
},
"dependencies": {
"@projectwallace/css-analyzer": "^9.9.1",
"@projectwallace/css-parser": "~0.18.1"
"@projectwallace/css-parser": "~0.18.6"
},
"devDependencies": {
"@types/node": "^26.1.1",
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

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

3 changes: 3 additions & 0 deletions src/configs/correctness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export default {
'projectwallace/no-invalid-z-index': true,
'projectwallace/no-unknown-container-names': true,
'no-unknown-custom-properties': true,
'projectwallace/no-missing-if-else': true,
'projectwallace/no-unreachable-if-branches': true,
'projectwallace/no-useless-if': true,
'projectwallace/no-unreachable-media-conditions': true,
'projectwallace/no-duplicate-anchor-names': true,
'projectwallace/no-duplicate-container-names': true,
Expand Down
3 changes: 3 additions & 0 deletions src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export default {
'projectwallace/no-static-media-queries': true,
'projectwallace/no-unknown-container-names': true,
'no-unknown-custom-properties': true,
'projectwallace/no-missing-if-else': true,
'projectwallace/no-unreachable-if-branches': true,
'projectwallace/no-useless-if': true,
'projectwallace/no-unreachable-media-conditions': true,
'projectwallace/no-unused-container-names': true,
'projectwallace/no-unused-custom-properties': true,
Expand Down
3 changes: 3 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,21 @@ test('exports an array of stylelint rules', () => {
'projectwallace/no-duplicate-keyframes',
'projectwallace/no-duplicate-registered-properties',
'projectwallace/no-invalid-z-index',
'projectwallace/no-missing-if-else',
'projectwallace/no-property-browserhacks',
'projectwallace/no-property-shorthand',
'projectwallace/no-pseudo-elements-in-is-where',
'projectwallace/no-static-container-queries',
'projectwallace/no-static-media-queries',
'projectwallace/no-unknown-container-names',
'projectwallace/no-unreachable-if-branches',
'projectwallace/no-unreachable-media-conditions',
'projectwallace/no-unused-container-names',
'projectwallace/no-unused-custom-properties',
'projectwallace/no-unused-keyframes',
'projectwallace/no-unused-layers',
'projectwallace/no-useless-custom-property-assignment',
'projectwallace/no-useless-if',
'projectwallace/no-value-browserhacks',
])
})
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,22 @@ import no_duplicate_data_urls from './rules/no-duplicate-data-urls/index.js'
import no_duplicate_keyframes from './rules/no-duplicate-keyframes/index.js'
import no_duplicate_registered_properties from './rules/no-duplicate-registered-properties/index.js'
import no_invalid_z_index from './rules/no-invalid-z-index/index.js'
import no_missing_if_else from './rules/no-missing-if-else/index.js'
import no_pseudo_elements_in_is_where from './rules/no-pseudo-elements-in-is-where/index.js'
import no_property_browserhacks from './rules/no-property-browserhacks/index.js'
import no_property_shorthand from './rules/no-property-shorthand/index.js'
import no_value_browserhacks from './rules/no-value-browserhacks/index.js'
import no_static_container_queries from './rules/no-static-container-queries/index.js'
import no_static_media_queries from './rules/no-static-media-queries/index.js'
import no_unknown_container_names from './rules/no-unknown-container-names/index.js'
import no_unreachable_if_branches from './rules/no-unreachable-if-branches/index.js'
import no_unreachable_media_conditions from './rules/no-unreachable-media-conditions/index.js'
import no_unused_container_names from './rules/no-unused-container-names/index.js'
import no_unused_custom_properties from './rules/no-unused-custom-properties/index.js'
import no_unused_keyframes from './rules/no-unused-keyframes/index.js'
import no_unused_layers from './rules/no-unused-layers/index.js'
import no_useless_custom_property_assignment from './rules/no-useless-custom-property-assignment/index.js'
import no_useless_if from './rules/no-useless-if/index.js'

// Alphabetically ordered list of all plugins
const plugins: stylelint.Plugin[] = [
Expand Down Expand Up @@ -103,18 +106,21 @@ const plugins: stylelint.Plugin[] = [
no_duplicate_keyframes,
no_duplicate_registered_properties,
no_invalid_z_index,
no_missing_if_else,
no_property_browserhacks,
no_property_shorthand,
no_pseudo_elements_in_is_where,
no_static_container_queries,
no_static_media_queries,
no_unknown_container_names,
no_unreachable_if_branches,
no_unreachable_media_conditions,
no_unused_container_names,
no_unused_custom_properties,
no_unused_keyframes,
no_unused_layers,
no_useless_custom_property_assignment,
no_useless_if,
no_value_browserhacks,
]

Expand Down
61 changes: 61 additions & 0 deletions src/rules/no-missing-if-else/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# No missing if() else

Require every `if()` function to include an `else` condition.

<!-- prettier-ignore -->
```css
a {
color: if(
style(--dark): white;
style(--light): black
/* ^^^^^^^^^^^^^^^^^^^^^ */
);
}
```

The `if()` function evaluates its conditions in order and uses the value of the first one that matches. Without an `else` branch, none of the conditions may match, in which case the property is treated as if `if()` had returned the [guaranteed-invalid value](https://www.w3.org/TR/css-values-4/#guaranteed-invalid) — silently dropping the declaration. Adding an `else` branch guarantees a fallback value is always available.

## Options

### `true`

The following are considered problems:

<!-- prettier-ignore -->
```css
a {
color: if(
style(--dark): white;
style(--light): black
);
}

a {
width: if(
media(width > 600px): 50%
);
}
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
```css
a {
color: if(
style(--dark): white;
else: black
);
}

a {
width: if(
media(width > 600px): 50%;
else: 100%
);
}
```

## Prior art

- [CSS Values and Units Module Level 5: the `if()` function](https://drafts.csswg.org/css-values-5/#if-notation)
139 changes: 139 additions & 0 deletions src/rules/no-missing-if-else/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import stylelint from 'stylelint'
import { test, expect } from 'vitest'
import plugin from './index.js'

const rule_name = 'projectwallace/no-missing-if-else'

const config = {
plugins: [plugin],
rules: {
[rule_name]: true,
},
}

// ---------------------------------------------------------------------------
// No violation
// ---------------------------------------------------------------------------

test('should not error when if() has an else condition', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white; else: black); }`,
config,
})

expect(errored).toBe(false)
expect(warnings).toStrictEqual([])
})

test('should not error when if() has multiple conditions and an else condition', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white; style(--light): black; else: gray); }`,
config,
})

expect(errored).toBe(false)
expect(warnings).toStrictEqual([])
})

test('should not error when if() has multiple else conditions', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white; else: black; else: red); }`,
config,
})

expect(errored).toBe(false)
expect(warnings).toStrictEqual([])
})

test('should not error when there is no if() function', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { color: red; }`,
config,
})

expect(errored).toBe(false)
expect(warnings).toStrictEqual([])
})

// ---------------------------------------------------------------------------
// Violation
// ---------------------------------------------------------------------------

test('should error when if() has no else condition', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white; style(--light): black); }`,
config,
})

expect(errored).toBe(true)
expect(warnings.length).toBe(1)

const [{ text }] = warnings
expect(text).toBe(`Expected if() to have an else condition (${rule_name})`)
})

test('should error when if() has a single condition and no else', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { width: if(media(width > 600px): 50%); }`,
config,
})

expect(errored).toBe(true)
expect(warnings.length).toBe(1)
})

test('should error for each if() function missing an else condition', async () => {
const {
results: [{ warnings, errored }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white); background: if(style(--x): red); }`,
config,
})

expect(errored).toBe(true)
expect(warnings.length).toBe(2)
})

test('should report the correct position of the if() function', async () => {
const {
results: [{ warnings }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white); }`,
config,
})

expect(warnings[0]).toMatchObject({
line: 1,
column: 12,
endColumn: 36,
})
})

test('should not run when primary option is invalid', async () => {
const {
results: [{ warnings, invalidOptionWarnings }],
} = await stylelint.lint({
code: `a { color: if(style(--dark): white); }`,
config: {
plugins: [plugin],
rules: {
[rule_name]: [2],
},
},
})

expect(warnings).toStrictEqual([])
expect(invalidOptionWarnings.length).toBeGreaterThan(0)
})
Loading
Loading