Skip to content

Commit

Permalink
eslint
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelangeloio committed Jan 2, 2024
1 parent 5f45eff commit cf94192
Show file tree
Hide file tree
Showing 8 changed files with 1,306 additions and 47 deletions.
4 changes: 4 additions & 0 deletions eslint/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
out
README.md
LICENSE.txt
assets
39 changes: 39 additions & 0 deletions eslint/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Changelog

## [0.3.3](https://github.com/michaelangeloio/does-it-throw/compare/does-it-throw-lsp-v0.3.2...does-it-throw-lsp-v0.3.3) (2023-12-24)


### Bug Fixes

* catch with throw statement not included ([#95](https://github.com/michaelangeloio/does-it-throw/issues/95)) ([fd223db](https://github.com/michaelangeloio/does-it-throw/commit/fd223db4f56e87439999b9b33a393769bd2b7c5b))

* **deps-dev:** remove remaining eslint dev dependencies ([#97](https://github.com/michaelangeloio/does-it-throw/issues/97)) ([5f173a6](https://github.com/michaelangeloio/does-it-throw/commit/5f173a69cb86570a526a665d453b86ae776538d0))

## [0.3.2](https://github.com/michaelangeloio/does-it-throw/compare/does-it-throw-lsp-v0.3.1...does-it-throw-lsp-v0.3.2) (2023-12-17)


### Bug Fixes

* update server package.json keywords ([#87](https://github.com/michaelangeloio/does-it-throw/issues/87)) ([c19717d](https://github.com/michaelangeloio/does-it-throw/commit/c19717d96a09152d959bfd7d5c3a34ac62f5e26d))

## [0.3.1](https://github.com/michaelangeloio/does-it-throw/compare/does-it-throw-lsp-v0.3.0...does-it-throw-lsp-v0.3.1) (2023-12-17)


### Bug Fixes

* functions and throw statements are underlined even if caught ([#81](https://github.com/michaelangeloio/does-it-throw/issues/81)) ([16adf85](https://github.com/michaelangeloio/does-it-throw/commit/16adf85b05b92542fa6c09ac1611dd56c7603c99))

## [0.3.0](https://github.com/michaelangeloio/does-it-throw/compare/does-it-throw-lsp-v0.2.5...does-it-throw-lsp-v0.3.0) (2023-12-16)


### Features

* neovim support ([#78](https://github.com/michaelangeloio/does-it-throw/issues/78)) ([6152786](https://github.com/michaelangeloio/does-it-throw/commit/61527869e70f54e99616375f7efd53b24e0fa01a))


### Bug Fixes

* add biome for standardization, ensure the builder reports errors correctly ([#72](https://github.com/michaelangeloio/does-it-throw/issues/72)) ([0d18392](https://github.com/michaelangeloio/does-it-throw/commit/0d18392268516abb79d015f90495dd331e7ef998))
* re-organize primary crate into modules ([#42](https://github.com/michaelangeloio/does-it-throw/issues/42)) ([badb106](https://github.com/michaelangeloio/does-it-throw/commit/badb1061d0dfc679458d55609e43cccfdca01794))
* results should still show even if file cannot resolve (calls to throws) ([#76](https://github.com/michaelangeloio/does-it-throw/issues/76)) ([f908556](https://github.com/michaelangeloio/does-it-throw/commit/f908556dfda8eca9195c87269fac71bc6d3e8bf9))
* update details, fix logic in some call expressions, including spread operators ([#40](https://github.com/michaelangeloio/does-it-throw/issues/40)) ([cdfdf47](https://github.com/michaelangeloio/does-it-throw/commit/cdfdf47a2d657364abc1b3b3ce97e89405b842b3))
55 changes: 55 additions & 0 deletions eslint/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "does-it-throw-eslint",
"description": "ESLint Plugin for Does It Throw",
"version": "0.3.3",
"author": {
"name": "Michael Angelo",
"email": "[email protected]"
},
"license": "MIT",
"licenses": [
{
"type": "MIT",
"url": "https://github.com/michaelangeloio/does-it-throw/blob/main/LICENSE"
}
],
"engines": {
"node": "*"
},
"repository": "https://github.com/michaelangeloio/does-it-throw",
"categories": [
"Programming Languages",
"Linters",
"Debuggers"
],
"keywords": [
"does it throw",
"throw finder",
"throw",
"javascript",
"typescript",
"lsp",
"language server",
"exceptions",
"extension"
],
"qna": "https://github.com/michaelangeloio/does-it-throw/discussions",
"dependencies": {
"@typescript-eslint/utils": "^6.17.0",
"eslint": "^8.56.0",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.8"
},
"files": [
"out",
"package.json",
"README.md",
"LICENSE.txt",
"bin"
],
"main": "out/server.js",
"bin": {
"does-it-throw-lsp": "./bin/does-it-throw"
},
"scripts": {}
}
261 changes: 261 additions & 0 deletions eslint/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import {
DidChangeConfigurationNotification,
InitializeParams,
InitializeResult,
ProposedFeatures,
TextDocumentSyncKind,
TextDocuments,
createConnection
} from 'vscode-languageserver/node'

import { access, constants, readFile } from 'fs/promises'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { InputData, ParseResult, parse_js } from './rust/does_it_throw_wasm'
import path = require('path')
import { inspect } from 'util'

const connection = createConnection(ProposedFeatures.all)

const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
let hasConfigurationCapability = false
let hasWorkspaceFolderCapability = false
// use if needed later
// let hasDiagnosticRelatedInformationCapability = false

let rootUri: string | undefined | null

connection.onInitialize((params: InitializeParams) => {
const capabilities = params.capabilities

hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration)
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders)
// use if needed later
// hasDiagnosticRelatedInformationCapability = !!(
// capabilities.textDocument &&
// capabilities.textDocument.publishDiagnostics &&
// capabilities.textDocument.publishDiagnostics.relatedInformation
// )

const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental
}
}
if (params?.workspaceFolders && params.workspaceFolders.length > 1) {
throw new Error('This extension only supports one workspace folder at this time')
}
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: false
}
}
}
if (!hasWorkspaceFolderCapability) {
rootUri = params.rootUri
} else {
rootUri = params?.workspaceFolders?.[0]?.uri
}

return result
})

connection.onInitialized(() => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(DidChangeConfigurationNotification.type, undefined)
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
connection.console.log(`Workspace folder change event received. ${JSON.stringify(_event)}`)
})
}
})

type DiagnosticSeverity = 'Error' | 'Warning' | 'Information' | 'Hint'

// The server settings
interface Settings {
maxNumberOfProblems: number
throwStatementSeverity: DiagnosticSeverity
functionThrowSeverity: DiagnosticSeverity
callToThrowSeverity: DiagnosticSeverity
callToImportedThrowSeverity: DiagnosticSeverity
includeTryStatementThrows: boolean
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: Settings = {
maxNumberOfProblems: 1000000,
throwStatementSeverity: 'Hint',
functionThrowSeverity: 'Hint',
callToThrowSeverity: 'Hint',
callToImportedThrowSeverity: 'Hint',
includeTryStatementThrows: false
}
// 👆 very unlikely someone will have more than 1 million throw statements, lol
// if you do, might want to rethink your code?
let globalSettings: Settings = defaultSettings

// Cache the settings of all open documents
const documentSettings: Map<string, Thenable<Settings>> = new Map()

connection.onDidChangeConfiguration((change) => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear()
} else {
globalSettings = <Settings>(change.settings.doesItThrow || defaultSettings)
}

// Revalidate all open text documents
// biome-ignore lint/complexity/noForEach: original vscode-languageserver code
documents.all().forEach(validateTextDocument)
})

function getDocumentSettings(resource: string): Thenable<Settings> {
if (!hasConfigurationCapability) {
connection.console.info(`does not have config capability, using global settings: ${JSON.stringify(globalSettings)}`)
return Promise.resolve(globalSettings)
}
let result = documentSettings.get(resource)
if (!result) {
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: 'doesItThrow'
})
documentSettings.set(resource, result)
}
return result
}

// Only keep settings for open documents
documents.onDidClose((e) => {
documentSettings.delete(e.document.uri)
})

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(async (change) => {
validateTextDocument(change.document)
})

documents.onDidSave((change) => {
validateTextDocument(change.document)
})

const _checkAccessOnFile = async (file: string) => {
try {
await access(file, constants.R_OK)
return Promise.resolve(file)
} catch (e) {
return Promise.reject(e)
}
}

const findFirstFileThatExists = async (uri: string, relative_import: string) => {
const isTs = uri.endsWith('.ts') || uri.endsWith('.tsx')
const baseUri = `${path.resolve(path.dirname(uri.replace('file://', '')), relative_import)}`
let files = Array(4)
if (isTs) {
files = [`${baseUri}.ts`, `${baseUri}.tsx`, `${baseUri}.js`, `${baseUri}.jsx`]
} else {
files = [`${baseUri}.js`, `${baseUri}.jsx`, `${baseUri}.ts`, `${baseUri}.tsx`]
}
return Promise.any(files.map(_checkAccessOnFile))
}

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
let settings = await getDocumentSettings(textDocument.uri)
if (!settings) {
// this should never happen, but just in case
connection.console.warn(`No settings found for ${textDocument.uri}, using defaults`)
settings = defaultSettings
}
try {
const opts = {
uri: textDocument.uri,
file_content: textDocument.getText(),
ids_to_check: [],
typescript_settings: {
decorators: true
},
function_throw_severity: settings?.functionThrowSeverity ?? defaultSettings.functionThrowSeverity,
throw_statement_severity: settings?.throwStatementSeverity ?? defaultSettings.throwStatementSeverity,
call_to_imported_throw_severity:
settings?.callToImportedThrowSeverity ?? defaultSettings.callToImportedThrowSeverity,
call_to_throw_severity: settings?.callToThrowSeverity ?? defaultSettings.callToThrowSeverity,
include_try_statement_throws: settings?.includeTryStatementThrows ?? defaultSettings.includeTryStatementThrows
} satisfies InputData
const analysis = parse_js(opts) as ParseResult

if (analysis.relative_imports.length > 0) {
const filePromises = analysis.relative_imports.map(async (relative_import) => {
try {
const file = await findFirstFileThatExists(textDocument.uri, relative_import)
return await readFile(file, 'utf-8')
} catch (e) {
connection.console.log(`Error reading file ${inspect(e)}`)
return undefined
}
})
const files = (await Promise.all(filePromises)).filter((file) => !!file)
const analysisArr = files.map((file) => {
if (!file) {
return undefined
}
const opts = {
uri: textDocument.uri,
file_content: file,
ids_to_check: [],
typescript_settings: {
decorators: true
}
} satisfies InputData
return parse_js(opts) as ParseResult
})
// TODO - this is a bit of a mess, but it works for now.
// The original analysis is the one that has the throw statements Map()
// We get the get the throw_ids from the imported analysis and then
// check the original analysis for existing throw_ids.
// This allows to to get the diagnostics from the imported analysis (one level deep for now)
for (const import_analysis of analysisArr) {
if (!import_analysis) {
return
}
if (import_analysis.throw_ids.length) {
for (const throw_id of import_analysis.throw_ids) {
const newDiagnostics = analysis.imported_identifiers_diagnostics.get(throw_id)
if (newDiagnostics?.diagnostics?.length) {
analysis.diagnostics.push(...newDiagnostics.diagnostics)
}
}
}
}
}
connection.sendDiagnostics({
uri: textDocument.uri,
diagnostics: analysis.diagnostics
})
} catch (e) {
console.log(e)
connection.console.error(`Error parsing file ${textDocument.uri}`)
connection.console.error(`settings are: ${JSON.stringify(settings)}`)
connection.console.error(`Error: ${e instanceof Error ? e.message : JSON.stringify(e)} error`)
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] })
}
}

connection.onDidChangeWatchedFiles((_change) => {
// Monitored files have change in VSCode
connection.console.log(`We received an file change event ${_change}, not implemented yet`)
})

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection)

// Listen on the connection
connection.listen()
22 changes: 22 additions & 0 deletions eslint/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2021",
"lib": [
"es2021",
"DOM"
],
"module": "ESNext",
"moduleResolution": "Bundler",
"sourceMap": true,
"strict": true,
"outDir": "out",
"rootDir": "src"
},
"include": [
"src"
],
"exclude": [
"node_modules",
".vscode-test"
]
}
Loading

0 comments on commit cf94192

Please sign in to comment.