diff --git a/.eslintignore b/.eslintignore
index d55c57aca..a46f06ecc 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,4 +7,5 @@
!/.vscode
!/.github
/prettier-playground
-/tests/fixtures/rules/indent/invalid/ts
\ No newline at end of file
+/tests/fixtures/rules/indent/invalid/ts
+/tests/fixtures/rules/valid-compile/valid/ts
diff --git a/README.md b/README.md
index c2a1c6753..2d07a1f9f 100644
--- a/README.md
+++ b/README.md
@@ -250,6 +250,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| [@ota-meshi/svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks.html) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
| [@ota-meshi/svelte/no-not-function-handler](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-not-function-handler.html) | disallow use of not function in event handler | :star: |
| [@ota-meshi/svelte/no-object-in-text-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-object-in-text-mustaches.html) | disallow objects in text mustache interpolation | :star: |
+| [@ota-meshi/svelte/valid-compile](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-compile.html) | disallow warnings when compiling. | |
## Security Vulnerability
diff --git a/docs/rules/README.md b/docs/rules/README.md
index edd61c36b..c6f047160 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -18,6 +18,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
| [@ota-meshi/svelte/no-not-function-handler](./no-not-function-handler.md) | disallow use of not function in event handler | :star: |
| [@ota-meshi/svelte/no-object-in-text-mustaches](./no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
+| [@ota-meshi/svelte/valid-compile](./valid-compile.md) | disallow warnings when compiling. | |
## Security Vulnerability
diff --git a/docs/rules/valid-compile.md b/docs/rules/valid-compile.md
new file mode 100644
index 000000000..8f592862b
--- /dev/null
+++ b/docs/rules/valid-compile.md
@@ -0,0 +1,46 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "@ota-meshi/svelte/valid-compile"
+description: "disallow warnings when compiling."
+---
+
+# @ota-meshi/svelte/valid-compile
+
+> disallow warnings when compiling.
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+This rule uses Svelte compiler to check the source code.
+
+
+
+
+
+```svelte
+
+
+
+
+
+
+
+```
+
+
+
+Note that we exclude reports for some checks, such as `missing-declaration`, which you can check with different ESLint rules.
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/valid-compile.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/valid-compile.ts)
diff --git a/package.json b/package.json
index 36e9fbb52..04c782329 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"dependencies": {
"debug": "^4.3.1",
"eslint-utils": "^3.0.0",
+ "sourcemap-codec": "^1.4.8",
"svelte-eslint-parser": "^0.4.0"
},
"peerDependencies": {
@@ -82,6 +83,9 @@
"eslint-plugin-vue": "^7.9.0",
"eslint-plugin-yml": "^0.9.0",
"eslint4b": "^7.25.0",
+ "estree-walker": "^3.0.0",
+ "locate-character": "^2.0.5",
+ "magic-string": "^0.25.7",
"mocha": "^9.0.0",
"nyc": "^15.1.0",
"pako": "^2.0.3",
diff --git a/src/rules/valid-compile.ts b/src/rules/valid-compile.ts
new file mode 100644
index 000000000..73abc5324
--- /dev/null
+++ b/src/rules/valid-compile.ts
@@ -0,0 +1,420 @@
+import Module from "module"
+import path from "path"
+import { VisitorKeys } from "svelte-eslint-parser"
+import type { SourceCode } from "../types"
+import { createRule } from "../utils"
+import * as compiler from "svelte/compiler"
+import type typescript from "typescript"
+import type { SourceMapMappings } from "sourcemap-codec"
+import { decode } from "sourcemap-codec"
+
+type TS = typeof typescript
+
+type Warning = {
+ code?: string
+ start?: {
+ line: number
+ column: number
+ }
+ end?: {
+ line: number
+ column: number
+ }
+ message: string
+}
+
+class LinesAndColumns {
+ private readonly lineStartIndices: number[]
+
+ public constructor(code: string) {
+ const len = code.length
+ const lineStartIndices = [0]
+ for (let index = 0; index < len; index++) {
+ const c = code[index]
+ if (c === "\r") {
+ const next = code[index + 1] || ""
+ if (next === "\n") {
+ index++
+ }
+ lineStartIndices.push(index + 1)
+ } else if (c === "\n") {
+ lineStartIndices.push(index + 1)
+ }
+ }
+ this.lineStartIndices = lineStartIndices
+ }
+
+ public getLocFromIndex(index: number): { line: number; column: number } {
+ const lineNumber = sortedLastIndex(this.lineStartIndices, index)
+ return {
+ line: lineNumber,
+ column: index - this.lineStartIndices[lineNumber - 1],
+ }
+ }
+
+ public getIndexFromLoc(loc: { line: number; column: number }): number {
+ const lineStartIndex = this.lineStartIndices[loc.line - 1]
+ const positionIndex = lineStartIndex + loc.column
+
+ return positionIndex
+ }
+}
+
+export default createRule("valid-compile", {
+ meta: {
+ docs: {
+ description: "disallow warnings when compiling.",
+ category: "Possible Errors",
+ recommended: false,
+ },
+ schema: [],
+ messages: {},
+ type: "problem",
+ },
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ const text = sourceCode.text
+
+ const ignores = ["missing-declaration"]
+
+ /**
+ * report
+ */
+ function report(
+ warnings: Warning[],
+ mapLocation?: (warn: Warning) => {
+ start?: {
+ line: number
+ column: number
+ }
+ end?: {
+ line: number
+ column: number
+ }
+ },
+ ) {
+ for (const warn of warnings) {
+ if (warn.code && ignores.includes(warn.code)) {
+ continue
+ }
+ const loc = mapLocation?.(warn) ?? warn
+ context.report({
+ loc: {
+ start: loc.start || loc.end || { line: 1, column: 0 },
+ end: loc.end || loc.start || { line: 1, column: 0 },
+ },
+ message: `${warn.message}${warn.code ? `(${warn.code})` : ""}`,
+ })
+ }
+ }
+
+ const parserVisitorKeys = sourceCode.visitorKeys
+ if (isEqualKeys(parserVisitorKeys, VisitorKeys)) {
+ return {
+ "Program:exit"() {
+ report(getWarnings(text))
+ },
+ }
+ }
+ let ts: TS
+ try {
+ const createRequire: (filename: string) => (modName: string) => unknown =
+ // Added in v12.2.0
+ Module.createRequire ||
+ // Added in v10.12.0, but deprecated in v12.2.0.
+ Module.createRequireFromPath
+
+ const cwd = context.getCwd?.() ?? process.cwd()
+ const relativeTo = path.join(cwd, "__placeholder__.js")
+ ts = createRequire(relativeTo)("typescript") as TS
+ } catch {
+ return {}
+ }
+
+ class RemapContext {
+ private originalStart = 0
+
+ private code = ""
+
+ private locs: LinesAndColumns | null = null
+
+ private readonly mapIndexes: {
+ range: [number, number]
+ remap: (index: number) => number
+ }[] = []
+
+ public appendOriginal(endIndex: number) {
+ const codeStart = this.code.length
+ const start = this.originalStart
+ this.code += text.slice(start, endIndex)
+ this.originalStart = endIndex
+ const offset = start - codeStart
+ this.mapIndexes.push({
+ range: [codeStart, this.code.length],
+ remap(index) {
+ return index + offset
+ },
+ })
+ }
+
+ public postprocess(): string {
+ this.appendOriginal(text.length)
+ return this.code
+ }
+
+ public appendTranspile(endIndex: number) {
+ const codeStart = this.code.length
+ const start = this.originalStart
+ const inputText = text.slice(start, endIndex)
+
+ const output = ts.transpileModule(inputText, {
+ reportDiagnostics: false,
+ compilerOptions: {
+ target: ts.ScriptTarget.ESNext,
+ importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
+ sourceMap: true,
+ },
+ transformers: {
+ before: [createTsImportTransformer(ts)],
+ },
+ })
+
+ const outputText = `${output.outputText}\n`
+
+ this.code += outputText
+ this.originalStart = endIndex
+
+ let outputLocs: LinesAndColumns | null = null
+ let inputLocs: LinesAndColumns | null = null
+ let decoded: SourceMapMappings | null = null
+ this.mapIndexes.push({
+ range: [codeStart, this.code.length],
+ remap: (index) => {
+ outputLocs ??= new LinesAndColumns(outputText)
+ inputLocs ??= new LinesAndColumns(inputText)
+ const outputCodePos = outputLocs.getLocFromIndex(index - codeStart)
+ const inputCodePos = remapPosition(outputCodePos)
+ return inputLocs.getIndexFromLoc(inputCodePos) + start
+ },
+ })
+
+ /** Remapping source position */
+ function remapPosition(pos: { line: number; column: number }): {
+ line: number
+ column: number
+ } {
+ decoded ??= decode(JSON.parse(output.sourceMapText!).mappings)
+
+ const lineMaps = decoded[pos.line]
+
+ if (!lineMaps?.length) {
+ for (let line = pos.line - 1; line >= 0; line--) {
+ const prevLineMaps = decoded[line]
+ if (prevLineMaps?.length) {
+ const [, , remapLine, remapCol] =
+ prevLineMaps[prevLineMaps.length - 1]
+ return {
+ line: remapLine!,
+ column: remapCol!,
+ }
+ }
+ }
+ return { line: -1, column: -1 }
+ }
+
+ for (let index = 0; index < lineMaps.length - 1; index++) {
+ const [col, , remapLine, remapCol] = lineMaps[index]
+ if (col <= pos.column && pos.column < lineMaps[index + 1][0]) {
+ return {
+ line: remapLine!,
+ column: remapCol!,
+ }
+ }
+ }
+ const [, , remapLine, remapCol] = lineMaps[lineMaps.length - 1]
+ return {
+ line: remapLine!,
+ column: remapCol!,
+ }
+ }
+ }
+
+ public remapLocs(points: {
+ start?: {
+ line: number
+ column: number
+ }
+ end?: {
+ line: number
+ column: number
+ }
+ }): {
+ start?: {
+ line: number
+ column: number
+ }
+ end?: {
+ line: number
+ column: number
+ }
+ } {
+ const locs = (this.locs ??= new LinesAndColumns(this.code))
+ let start:
+ | {
+ line: number
+ column: number
+ }
+ | undefined = undefined
+ let end:
+ | {
+ line: number
+ column: number
+ }
+ | undefined = undefined
+ if (points.start) {
+ const index = locs.getIndexFromLoc(points.start)
+ for (const mapIndex of this.mapIndexes) {
+ if (mapIndex.range[0] <= index && index < mapIndex.range[1]) {
+ start = sourceCode.getLocFromIndex(mapIndex.remap(index))
+ break
+ }
+ }
+ }
+ if (points.end) {
+ const index = locs.getIndexFromLoc(points.end)
+ for (const mapIndex of this.mapIndexes) {
+ if (mapIndex.range[0] <= index && index <= mapIndex.range[1]) {
+ end = sourceCode.getLocFromIndex(mapIndex.remap(index))
+ break
+ }
+ }
+ }
+
+ return { start, end }
+ }
+ }
+
+ const remapContext = new RemapContext()
+
+ return {
+ SvelteScriptElement(node) {
+ if (node.endTag) {
+ remapContext.appendOriginal(node.startTag.range[1])
+ remapContext.appendTranspile(node.endTag.range[0])
+ }
+ },
+ "Program:exit"() {
+ const code = remapContext.postprocess()
+ report(getWarnings(code), (warn) => remapContext.remapLocs(warn))
+ },
+ }
+ },
+})
+
+/**
+ * Get compile warnings
+ */
+function getWarnings(code: string): Warning[] {
+ try {
+ const result = compiler.compile(code, { generate: false })
+
+ return result.warnings
+ } catch (e) {
+ // console.log(code)
+ return [
+ {
+ message: e.message,
+ start: e.start,
+ end: e.end,
+ },
+ ]
+ }
+}
+
+/**
+ * Checks if the given visitorKeys are the equals.
+ */
+function isEqualKeys(
+ a: SourceCode["visitorKeys"],
+ b: SourceCode["visitorKeys"],
+): boolean {
+ const keysA = new Set(Object.keys(a))
+ const keysB = new Set(Object.keys(a))
+ if (keysA.size !== keysB.size) {
+ return false
+ }
+ for (const key of keysA) {
+ if (!keysB.has(key)) {
+ return false
+ }
+ const vKeysA = new Set(a[key])
+ const vKeysB = new Set(b[key])
+ if (vKeysA.size !== vKeysB.size) {
+ return false
+ }
+
+ for (const vKey of vKeysA) {
+ if (!vKeysB.has(vKey)) {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://github.com/sveltejs/eslint-plugin-svelte3/blob/259263ccaf69c59e473d9bfa39706b0955eccfbd/src/preprocess.js#L194
+ * MIT License @ Conduitry
+ */
+function createTsImportTransformer(
+ ts: TS,
+): typescript.TransformerFactory {
+ const factory = ts.factory
+ /**
+ * https://github.com/sveltejs/svelte-preprocess/blob/main/src/transformers/typescript.ts
+ * TypeScript transformer for preserving imports correctly when preprocessing TypeScript files
+ */
+ return (context: typescript.TransformationContext) => {
+ /** visitor */
+ function visit(node: typescript.Node): typescript.Node {
+ if (ts.isImportDeclaration(node)) {
+ if (node.importClause && node.importClause.isTypeOnly) {
+ return factory.createEmptyStatement()
+ }
+
+ return factory.createImportDeclaration(
+ node.decorators,
+ node.modifiers,
+ node.importClause,
+ node.moduleSpecifier,
+ )
+ }
+
+ return ts.visitEachChild(node, (child) => visit(child), context)
+ }
+
+ return (node: typescript.SourceFile) => ts.visitNode(node, visit)
+ }
+}
+
+/**
+ * Uses a binary search to determine the highest index at which value should be inserted into array in order to maintain its sort order.
+ */
+function sortedLastIndex(array: number[], value: number): number {
+ let lower = 0
+ let upper = array.length
+
+ while (lower < upper) {
+ const mid = Math.floor(lower + (upper - lower) / 2)
+ const target = array[mid]
+ if (target < value) {
+ lower = mid + 1
+ } else if (target > value) {
+ upper = mid
+ } else {
+ return mid + 1
+ }
+ }
+
+ return upper
+}
diff --git a/src/types.ts b/src/types.ts
index 75fd389bc..ca0526f03 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -125,6 +125,9 @@ export type RuleContext = {
markVariableAsUsed(name: string): boolean
report(descriptor: ReportDescriptor): void
+
+ // eslint@6 does not have this method.
+ getCwd?: () => string
}
export type NodeOrToken = {
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 5ef170ebe..e9d71effd 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -17,6 +17,7 @@ import preferClassDirective from "../rules/prefer-class-directive"
import shorthandAttribute from "../rules/shorthand-attribute"
import spacedHtmlComment from "../rules/spaced-html-comment"
import system from "../rules/system"
+import validCompile from "../rules/valid-compile"
export const rules = [
buttonHasType,
@@ -37,4 +38,5 @@ export const rules = [
shorthandAttribute,
spacedHtmlComment,
system,
+ validCompile,
] as RuleModule[]
diff --git a/tests/fixtures/rules/valid-compile/invalid/a11y01-errors.json b/tests/fixtures/rules/valid-compile/invalid/a11y01-errors.json
new file mode 100644
index 000000000..7865a3bf9
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/invalid/a11y01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "A11y:
element should have an alt attribute(a11y-missing-attribute)",
+ "line": 5,
+ "column": 1
+ }
+]
diff --git a/tests/fixtures/rules/valid-compile/invalid/a11y01-input.svelte b/tests/fixtures/rules/valid-compile/invalid/a11y01-input.svelte
new file mode 100644
index 000000000..a6a917dd2
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/invalid/a11y01-input.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/fixtures/rules/valid-compile/valid/test01-input.svelte b/tests/fixtures/rules/valid-compile/valid/test01-input.svelte
new file mode 100644
index 000000000..280c346fd
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/valid/test01-input.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/tests/fixtures/rules/valid-compile/valid/ts/class01-input.svelte b/tests/fixtures/rules/valid-compile/valid/ts/class01-input.svelte
new file mode 100644
index 000000000..5877c8e57
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/valid/ts/class01-input.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tests/fixtures/rules/valid-compile/valid/ts/test01-input.svelte b/tests/fixtures/rules/valid-compile/valid/ts/test01-input.svelte
new file mode 100644
index 000000000..59d83f68a
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/valid/ts/test01-input.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/tests/fixtures/rules/valid-compile/valid/undef01-input.svelte b/tests/fixtures/rules/valid-compile/valid/undef01-input.svelte
new file mode 100644
index 000000000..61a5b660d
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/valid/undef01-input.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/fixtures/rules/valid-compile/valid/unuse01-input.svelte b/tests/fixtures/rules/valid-compile/valid/unuse01-input.svelte
new file mode 100644
index 000000000..f6503a44c
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/valid/unuse01-input.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/tests/src/rules/valid-compile.ts b/tests/src/rules/valid-compile.ts
new file mode 100644
index 000000000..f19f0990d
--- /dev/null
+++ b/tests/src/rules/valid-compile.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/valid-compile"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ parser: {
+ ts: "@typescript-eslint/parser",
+ js: "espree",
+ },
+ },
+})
+
+tester.run("valid-compile", rule as any, loadTestCases("valid-compile"))