Skip to content

Commit b732ec6

Browse files
authored
feat: add svelte/@typescript-eslint/no-unnecessary-condition rule (#262)
* feat: wpi no-unnecessary-condition * docs: support type info in demo site * fix: rule * fix: revert eslint ignore * Create real-wasps-punch.md * fix: ignore eslint
1 parent 3dae5ab commit b732ec6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3310
-42
lines changed

.changeset/real-wasps-punch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": minor
3+
---
4+
5+
feat: add `svelte/@typescript-eslint/no-unnecessary-condition` rule

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
/tests/fixtures/rules/valid-compile/valid/babel
1212
/tests/fixtures/rules/valid-compile/valid/ts
1313
/tests/fixtures/rules/prefer-style-directive
14+
/tests/fixtures/rules/@typescript-eslint
1415
/.svelte-kit
1516
/svelte.config-dist.js
1617
/build

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,11 @@ These rules relate to style guidelines, and are therefore quite subjective:
322322

323323
## Extension Rules
324324

325-
These rules extend the rules provided by ESLint itself to work well in Svelte:
325+
These rules extend the rules provided by ESLint itself, or other plugins to work well in Svelte:
326326

327327
| Rule ID | Description | |
328328
|:--------|:------------|:---|
329+
| [svelte/@typescript-eslint/no-unnecessary-condition](https://ota-meshi.github.io/eslint-plugin-svelte/rules/@typescript-eslint/no-unnecessary-condition/) | disallow conditionals where the type is always truthy or always falsy | :wrench: |
329330
| [svelte/no-inner-declarations](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-inner-declarations/) | disallow variable or `function` declarations in nested blocks | :star: |
330331
| [svelte/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/) | disallow trailing whitespace at the end of lines | :wrench: |
331332

docs-svelte-kit/src/lib/components/ESLintCodeBlock.svelte

+22-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66
preprocess,
77
postprocess,
88
} from "../eslint/scripts/linter.js"
9+
import { loadTsParser } from "../eslint/scripts/ts-parser.js"
10+
import { loadModulesForBrowser } from "../../../../src/shared/svelte-compile-warns/transform/load-module"
911
10-
const linter = createLinter()
12+
const modulesForBrowser = loadModulesForBrowser()
13+
const loadLinter = createLinter()
14+
15+
let tsParser = null
1116
1217
let code = ""
1318
export let rules = {}
@@ -19,6 +24,18 @@
1924
preprocess,
2025
postprocess,
2126
}
27+
$: hasLangTs =
28+
/lang\s*=\s*(?:"ts"|ts|'ts'|"typescript"|typescript|'typescript')/u.test(
29+
code,
30+
)
31+
$: linter = modulesForBrowser.then(
32+
hasLangTs && !tsParser
33+
? async () => {
34+
tsParser = await loadTsParser()
35+
return loadLinter
36+
}
37+
: () => loadLinter,
38+
)
2239
let showDiff = fix
2340
2441
function onLintedResult(evt) {
@@ -48,6 +65,10 @@
4865
parserOptions: {
4966
ecmaVersion: 2020,
5067
sourceType: "module",
68+
parser: {
69+
ts: tsParser,
70+
typescript: tsParser,
71+
},
5172
},
5273
rules,
5374
env: {

docs-svelte-kit/src/lib/components/ESLintPlayground.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
preprocess,
1010
postprocess,
1111
} from "../eslint/scripts/linter.js"
12+
import { loadTsParser } from "../eslint/scripts/ts-parser.js"
1213
import { loadModulesForBrowser } from "../../../../src/shared/svelte-compile-warns/transform/load-module"
1314
let tsParser = null
1415
const linter = loadModulesForBrowser()
1516
.then(async () => {
16-
tsParser = await import("@typescript-eslint/parser")
17+
tsParser = await loadTsParser()
1718
})
1819
.then(() => {
1920
return createLinter()

docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte

+15-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
lint(linter, code, config, options)
3636
})
3737
38+
let lastResult = {}
39+
3840
async function lint(linter, code, config, options) {
3941
messageMap.clear()
4042
/* eslint-disable no-param-reassign -- ignore */
@@ -69,12 +71,23 @@
6971
fixedMessages: fixResult.messages,
7072
})
7173
72-
leftMarkers = await Promise.all(
74+
lastResult = { messages, fixResult }
75+
76+
const markers = await Promise.all(
7377
messages.map((m) => messageToMarker(m, messageMap)),
7478
)
75-
rightMarkers = await Promise.all(
79+
const fixedMarkers = await Promise.all(
7680
fixResult.messages.map((m) => messageToMarker(m)),
7781
)
82+
if (
83+
lastResult.messages !== messages ||
84+
lastResult.fixResult !== fixResult
85+
) {
86+
// If the result has changed, don't update the markers
87+
return
88+
}
89+
leftMarkers = markers
90+
rightMarkers = fixedMarkers
7891
}
7992
8093
function applyFix() {

docs-svelte-kit/src/lib/eslint/scripts/monaco-loader.js

+16-10
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,23 @@ function appendMonacoEditorScript() {
4545
let setupedMonaco = null
4646
let editorLoaded = null
4747

48-
export async function loadMonacoEditor() {
49-
await (setupedMonaco || (setupedMonaco = setupMonaco()))
48+
export function loadMonacoEngine() {
49+
return setupedMonaco || (setupedMonaco = setupMonaco())
50+
}
51+
export function loadMonacoEditor() {
5052
return (
5153
editorLoaded ||
52-
(editorLoaded = new Promise((resolve) => {
53-
if (typeof window !== "undefined") {
54-
// eslint-disable-next-line node/no-missing-require -- ignore
55-
window.require(["vs/editor/editor.main"], (r) => {
56-
resolve(r)
57-
})
58-
}
59-
}))
54+
(editorLoaded = loadModuleFromMonaco("vs/editor/editor.main"))
6055
)
6156
}
57+
58+
export async function loadModuleFromMonaco(moduleName) {
59+
await loadMonacoEngine()
60+
return new Promise((resolve) => {
61+
if (typeof window !== "undefined") {
62+
window.require([moduleName], (r) => {
63+
resolve(r)
64+
})
65+
}
66+
})
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type typescript from "typescript"
2+
import type tsvfs from "@typescript/vfs"
3+
type TS = typeof typescript
4+
type TSVFS = typeof tsvfs
5+
6+
/** Create Program */
7+
export function createProgram(
8+
{
9+
ts,
10+
compilerOptions,
11+
compilerHost,
12+
}: {
13+
ts: TS
14+
compilerOptions: typescript.CompilerOptions
15+
compilerHost: typescript.CompilerHost
16+
},
17+
options: { filePath: string },
18+
): typescript.Program {
19+
try {
20+
const program = ts.createProgram({
21+
rootNames: [options.filePath],
22+
options: compilerOptions,
23+
host: compilerHost,
24+
})
25+
return program
26+
} catch (e) {
27+
// eslint-disable-next-line no-console -- Demo debug
28+
console.error(e)
29+
throw e
30+
}
31+
}
32+
33+
export function createCompilerOptions(ts: TS): typescript.CompilerOptions {
34+
const compilerOptions: typescript.CompilerOptions = {
35+
target: ts.ScriptTarget.ESNext,
36+
module: ts.ModuleKind.ESNext,
37+
jsx: ts.JsxEmit.Preserve,
38+
strict: true,
39+
}
40+
compilerOptions.lib = [ts.getDefaultLibFileName(compilerOptions)]
41+
return compilerOptions
42+
}
43+
44+
export async function createVirtualCompilerHost(
45+
{
46+
ts,
47+
tsvfs,
48+
compilerOptions,
49+
}: {
50+
ts: TS
51+
tsvfs: TSVFS
52+
compilerOptions: typescript.CompilerOptions
53+
},
54+
{ filePath: targetFilePath }: { filePath: string },
55+
): Promise<{
56+
compilerHost: typescript.CompilerHost
57+
updateFile: (sourceFile: typescript.SourceFile) => boolean
58+
fsMap: Map<string, string>
59+
}> {
60+
const fsMap = await tsvfs.createDefaultMapFromCDN(
61+
{
62+
lib: Array.from(
63+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- use internal
64+
(ts as any).libMap.keys(),
65+
),
66+
},
67+
ts.version,
68+
true,
69+
ts,
70+
)
71+
const system = tsvfs.createSystem(fsMap)
72+
const host = tsvfs.createVirtualCompilerHost(system, compilerOptions, ts)
73+
// eslint-disable-next-line @typescript-eslint/unbound-method -- backup original
74+
const original = { getSourceFile: host.compilerHost.getSourceFile }
75+
host.compilerHost.getSourceFile = function (
76+
fileName,
77+
languageVersionOrOptions,
78+
...args
79+
) {
80+
if (targetFilePath === fileName) {
81+
// Exclude the target file from caching as it will be modified.
82+
const file = this.readFile(fileName) ?? ""
83+
return ts.createSourceFile(fileName, file, languageVersionOrOptions, true)
84+
}
85+
if (this.fileExists(fileName)) {
86+
return original.getSourceFile.apply(this, [
87+
fileName,
88+
languageVersionOrOptions,
89+
...args,
90+
])
91+
}
92+
// Avoid error
93+
// eslint-disable-next-line no-console -- Demo debug
94+
console.log(`Not exists: ${fileName}`)
95+
return undefined
96+
}
97+
return {
98+
...host,
99+
fsMap,
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { loadMonacoEngine } from "./monaco-loader"
2+
import {
3+
createProgram,
4+
createCompilerOptions,
5+
createVirtualCompilerHost,
6+
} from "./ts-create-program.mts"
7+
8+
let tsParserCache = null
9+
export function loadTsParser() {
10+
return (tsParserCache ??= loadTsParserImpl())
11+
}
12+
13+
async function loadTsParserImpl() {
14+
await loadMonacoEngine()
15+
const [ts, tsvfs, tsParser] = await Promise.all([
16+
import("typescript"),
17+
import("@typescript/vfs"),
18+
import("@typescript-eslint/parser"),
19+
])
20+
if (typeof window === "undefined") {
21+
return tsParser
22+
}
23+
window.define("typescript", ts)
24+
25+
const compilerOptions = createCompilerOptions(ts)
26+
const filePath = "/demo.ts"
27+
const host = await createVirtualCompilerHost(
28+
{ ts, tsvfs, compilerOptions },
29+
{ filePath },
30+
)
31+
return {
32+
parseForESLint(code, options) {
33+
host.fsMap.set(filePath, code)
34+
// Requires its own Program instance to provide full type information.
35+
const program = createProgram(
36+
{ ts, compilerHost: host.compilerHost, compilerOptions },
37+
{ filePath },
38+
)
39+
40+
try {
41+
const result = tsParser.parseForESLint(code, {
42+
...options,
43+
filePath: filePath.replace(/^\//u, ""),
44+
programs: [program],
45+
})
46+
return result
47+
} catch (e) {
48+
// eslint-disable-next-line no-console -- Demo debug
49+
console.error(e)
50+
throw e
51+
}
52+
},
53+
}
54+
}

docs/rules.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ These rules relate to style guidelines, and are therefore quite subjective:
7575

7676
## Extension Rules
7777

78-
These rules extend the rules provided by ESLint itself to work well in Svelte:
78+
These rules extend the rules provided by ESLint itself, or other plugins to work well in Svelte:
7979

8080
| Rule ID | Description | |
8181
|:--------|:------------|:---|
82+
| [svelte/@typescript-eslint/no-unnecessary-condition](./rules/@typescript-eslint/no-unnecessary-condition.md) | disallow conditionals where the type is always truthy or always falsy | :wrench: |
8283
| [svelte/no-inner-declarations](./rules/no-inner-declarations.md) | disallow variable or `function` declarations in nested blocks | :star: |
8384
| [svelte/no-trailing-spaces](./rules/no-trailing-spaces.md) | disallow trailing whitespace at the end of lines | :wrench: |
8485

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/@typescript-eslint/no-unnecessary-condition"
5+
description: "disallow conditionals where the type is always truthy or always falsy"
6+
---
7+
8+
# svelte/@typescript-eslint/no-unnecessary-condition
9+
10+
> disallow conditionals where the type is always truthy or always falsy
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14+
15+
## :book: Rule Details
16+
17+
This rule extends the base `@typescript-eslint`'s [@typescript-eslint/no-unnecessary-condition] rule.
18+
The [@typescript-eslint/no-unnecessary-condition] rule does not understand reactive or rerendering of Svelte components and has false positives when used with Svelte components. This rule understands reactive and rerendering of Svelte components.
19+
20+
<ESLintCodeBlock fix>
21+
22+
<!--eslint-skip-->
23+
24+
```svelte
25+
<script lang="ts">
26+
/* eslint svelte/@typescript-eslint/no-unnecessary-condition: "error" */
27+
export let foo: number | null = null
28+
/* ✗ BAD */
29+
let b = foo || 42
30+
/* ✓ GOOD */
31+
$: a = foo || 42
32+
</script>
33+
34+
<!-- ✓ GOOD -->
35+
{foo || 42}
36+
```
37+
38+
</ESLintCodeBlock>
39+
40+
## :wrench: Options
41+
42+
```json
43+
{
44+
"@typescript-eslint/no-unnecessary-condition": "off",
45+
"svelte/@typescript-eslint/no-unnecessary-condition": [
46+
"error",
47+
{
48+
"allowConstantLoopConditions": false,
49+
"allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing": false
50+
}
51+
]
52+
}
53+
```
54+
55+
Same as [@typescript-eslint/no-unnecessary-condition] rule option. See [here](https://typescript-eslint.io/rules/no-unnecessary-condition/#options) for details.
56+
57+
## :couple: Related rules
58+
59+
- [@typescript-eslint/no-unnecessary-condition]
60+
61+
[@typescript-eslint/no-unnecessary-condition]: https://typescript-eslint.io/rules/no-unnecessary-condition/
62+
63+
## :mag: Implementation
64+
65+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/@typescript-eslint/no-unnecessary-condition.ts)
66+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/@typescript-eslint/no-unnecessary-condition.ts)
67+
68+
<sup>Taken with ❤️ [from @typescript-eslint/eslint-plugin](https://typescript-eslint.io/rules/no-unnecessary-condition/)</sup>

0 commit comments

Comments
 (0)