Skip to content

Commit

Permalink
save
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelangeloio committed Jan 6, 2024
1 parent cf94192 commit 078bc35
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 330 deletions.
5 changes: 4 additions & 1 deletion eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,8 @@
"bin": {
"does-it-throw-lsp": "./bin/does-it-throw"
},
"scripts": {}
"scripts": {},
"devDependencies": {
"@types/eslint": "^8.56.1"
}
}
300 changes: 41 additions & 259 deletions eslint/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,261 +1,43 @@
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)
}
import { ESLintUtils } from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(
name => `https://example.com/rule/${name}`,

);

// Type: RuleModule<"uppercase", ...>
export const rule = createRule({
create(context) {
return {
FunctionDeclaration(node) {
if (node.id != null) {
if (/^[a-z]/.test(node.id.name)) {
context.report({
messageId: 'uppercase',
node: node.id,
});
context.report({
messageId
loc: {

}
})
}
}
}
}
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()
},
};
},
name: 'uppercase-first-declarations',
meta: {
docs: {
description:
'Function declaration names should start with an upper-case letter.',
},
messages: {
uppercase: 'Start this name with an upper-case letter.',
},b
type: 'suggestion',
schema: [],
},
defaultOptions: [],
});
3 changes: 1 addition & 2 deletions eslint/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
"sourceMap": true,
"strict": true,
"outDir": "out",
"rootDir": "src"
},
"include": [
"src"
],
"exclude": [
"node_modules",
".vscode-test"
]
],
}
20 changes: 20 additions & 0 deletions package-lock.json

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

Loading

0 comments on commit 078bc35

Please sign in to comment.