From 8a7e81ba409d0929b44e2ca1245f0dfde2210c06 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 1 May 2024 10:42:13 +0800 Subject: [PATCH] feat: add ESLint integration (#171) --- packages/eslint/LICENSE | 21 ++++++ packages/eslint/index.ts | 117 ++++++++++++++++++++++++++++++++++ packages/eslint/package.json | 19 ++++++ packages/eslint/tsconfig.json | 7 ++ pnpm-lock.yaml | 25 ++++++++ tsconfig.json | 1 + 6 files changed, 190 insertions(+) create mode 100644 packages/eslint/LICENSE create mode 100644 packages/eslint/index.ts create mode 100644 packages/eslint/package.json create mode 100644 packages/eslint/tsconfig.json diff --git a/packages/eslint/LICENSE b/packages/eslint/LICENSE new file mode 100644 index 00000000..cb8d02b6 --- /dev/null +++ b/packages/eslint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present Johnson Chu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/eslint/index.ts b/packages/eslint/index.ts new file mode 100644 index 00000000..57393c2d --- /dev/null +++ b/packages/eslint/index.ts @@ -0,0 +1,117 @@ +import { FileMap, LanguagePlugin, VirtualCode, createLanguage, forEachEmbeddedCode, isDiagnosticsEnabled } from '@volar/language-core'; +import type { Linter } from 'eslint'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +const windowsPath = /\\/g; + +export function createProcessor( + languagePlugins: LanguagePlugin[], + caseSensitive: boolean, + extensionsMap: Record = { + 'javascript': '.js', + 'typescript': '.ts', + 'javascriptreact': '.jsx', + 'typescriptreact': '.tsx', + 'css': '.css', + 'less': '.less', + 'scss': '.scss', + 'sass': '.sass', + 'postcss': '.pcss', + 'stylus': '.styl', + 'html': '.html', + 'pug': '.pug', + 'json': '.json', + 'jsonc': '.json', + 'yaml': '.yaml', + 'markdown': '.md', + }, + supportsAutofix = true, +): Linter.Processor { + const language = createLanguage(languagePlugins, caseSensitive, () => { }); + const documents = new FileMap<{ + sourceDocument: TextDocument; + embeddedDocuments: TextDocument[]; + codes: VirtualCode[]; + }>(caseSensitive); + return { + supportsAutofix, + preprocess(text, filename) { + filename = filename.replace(windowsPath, '/'); + const files: Linter.ProcessorFile[] = []; + const sourceScript = language.scripts.set(filename, { + getLength() { + return text.length; + }, + getText(start, end) { + return text.substring(start, end); + }, + getChangeRange() { + return undefined; + }, + }); + if (sourceScript?.generated) { + const codes = []; + const embeddedDocuments = []; + for (const code of forEachEmbeddedCode(sourceScript.generated.root)) { + if (code.mappings.some(mapping => isDiagnosticsEnabled(mapping.data))) { + const ext = extensionsMap[code.languageId]; + if (!ext) { + continue; + } + files.push({ + filename: filename + ext, + text: code.snapshot.getText(0, code.snapshot.getLength()), + }); + codes.push(code); + embeddedDocuments.push(TextDocument.create(filename + ext, code.languageId, 0, code.snapshot.getText(0, code.snapshot.getLength()))); + } + } + documents.set(filename, { + sourceDocument: TextDocument.create(filename, sourceScript.languageId, 0, text), + embeddedDocuments, + codes, + }); + } + return files; + }, + postprocess(messagesArr, filename) { + filename = filename.replace(windowsPath, '/'); + const docs = documents.get(filename); + if (docs) { + const { codes, sourceDocument, embeddedDocuments } = docs; + for (let i = 0; i < messagesArr.length; i++) { + const code = codes[i]; + const map = language.maps.get(code); + if (!map) { + messagesArr[i].length = 0; + continue; + } + const embeddedDocument = embeddedDocuments[i]; + messagesArr[i] = messagesArr[i].filter(message => { + const start = embeddedDocument.offsetAt({ line: message.line - 1, character: message.column - 1 }); + const end = embeddedDocument.offsetAt({ line: (message.endLine ?? message.line) - 1, character: (message.endColumn ?? message.column) - 1 }); + for (const [sourceStart, mapping] of map.getSourceOffsets(start)) { + if (isDiagnosticsEnabled(mapping.data)) { + for (const [sourceEnd, mapping] of map.getSourceOffsets(end)) { + if (isDiagnosticsEnabled(mapping.data)) { + const sourcePosition = sourceDocument.positionAt(sourceStart); + const sourceEndPosition = sourceDocument.positionAt(sourceEnd); + message.line = sourcePosition.line + 1; + message.column = sourcePosition.character + 1; + message.endLine = sourceEndPosition.line + 1; + message.endColumn = sourceEndPosition.character + 1; + return true; + } + } + break; + } + } + return false; + }); + } + return messagesArr.flat(); + } + return []; + }, + }; +} diff --git a/packages/eslint/package.json b/packages/eslint/package.json new file mode 100644 index 00000000..4ae1852e --- /dev/null +++ b/packages/eslint/package.json @@ -0,0 +1,19 @@ +{ + "name": "@volar/eslint", + "version": "2.2.0-alpha.12", + "license": "MIT", + "files": [ + "**/*.js", + "**/*.d.ts" + ], + "repository": { + "type": "git", + "url": "https://github.com/volarjs/volar.js.git", + "directory": "packages/eslint" + }, + "dependencies": { + "@types/eslint": "^8.56.10", + "@volar/language-core": "2.2.0-alpha.12", + "vscode-languageserver-textdocument": "^1.0.11" + } +} diff --git a/packages/eslint/tsconfig.json b/packages/eslint/tsconfig.json new file mode 100644 index 00000000..6ac59ea5 --- /dev/null +++ b/packages/eslint/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ "*", "lib/**/*" ], + "references": [ + { "path": "../language-core/tsconfig.json" }, + ], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80a37f43..1efaa6d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,18 @@ importers: specifier: latest version: 2.15.0 + packages/eslint: + dependencies: + '@types/eslint': + specifier: ^8.56.10 + version: 8.56.10 + '@volar/language-core': + specifier: 2.2.0-alpha.12 + version: link:../language-core + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.11 + packages/kit: dependencies: '@volar/language-service': @@ -865,9 +877,15 @@ packages: resolution: {integrity: sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==} engines: {node: ^16.14.0 || >=18.0.0} + '@types/eslint@8.56.10': + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} @@ -3491,8 +3509,15 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.4 + '@types/eslint@8.56.10': + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + '@types/estree@1.0.5': {} + '@types/json-schema@7.0.15': {} + '@types/node@20.12.7': dependencies: undici-types: 5.26.5 diff --git a/tsconfig.json b/tsconfig.json index 62812f89..5937495b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ }, "include": [ "packages/*/tests" ], "references": [ + { "path": "./packages/eslint/tsconfig.json" }, { "path": "./packages/kit/tsconfig.json" }, { "path": "./packages/typescript/tsconfig.json" }, { "path": "./packages/vscode/tsconfig.json" },