Skip to content

Commit

Permalink
Lint fixer (#28)
Browse files Browse the repository at this point in the history
* implement fixer

* implement replaceText on fixer

* refactor insertText

* implement fix on reserved-word-case rule

* enable to return array of result from rule function

* implement fix on column-new-line rule

* implement fix on align-column-to-the-first

* aligne columns position on fix rule of column-new-line

* implement fix on linebreakAfterClauseKeyword

* indent for fixing lineBreakAfterClauseKeyWord

* implement fix on spaceSurroundingOperators

* implement fix on whereClauseNewLine

* fix whereClauseNewLineRule

* implement new rule align-where-clause-to-the-first

* refactor creating fix description logic

* implement fix

* implement quickFixCodeAction

* back to previous code to fix accidental changes

* use textDocument.getText instead of own cache

* enable to execute fix all problems command

* add test case to check the bug

* fix the bug fix doesn't work correctly

column-new-line rule was conflicted with align-column-to-the-first

* return the most smallest distance lint result to fix

* write README
  • Loading branch information
joe-re authored Jun 18, 2020
1 parent fabeaa8 commit afc5ec4
Show file tree
Hide file tree
Showing 33 changed files with 913 additions and 191 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ If you have multiple connection information on personal config file, you can swt

[VSC extension](https://marketplace.visualstudio.com/items?itemName=joe-re.sql-language-server) provides `Switch database connection` command.

Raw RPC param is:
Raw RPC param:
```
method: workspace/executeCommand
command: switchDataBaseConnection
Expand All @@ -244,6 +244,17 @@ Please refer this to know how to use and how to configure to make them be matche

![sqlint-on-editor](https://user-images.githubusercontent.com/4954534/83353304-3c3f1880-a384-11ea-8266-4d7048461b56.png)

Also you can use it to fix your problem if it's possible.

![2020-06-18_08-24-03](https://user-images.githubusercontent.com/4954534/84964358-84a95500-b13e-11ea-9c4f-0b787306bbdf.gif)

Raw RPC param:
```
method: workspace/executeCommand
command: fixAllFixableProblems
arguments: string(document uri)
```

### TODO

- [x] SELECT
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@
"vscode": "^1.45.1"
},
"activationEvents": [
"onLanguage:sql",
"extension.switchDatabaseConnection"
"onLanguage:sql"
],
"contributes": {
"commands": [
{
"command": "extension.switchDatabaseConnection",
"title": "SQLLanguageServer: Switch database connection"
"title": "Switch database connection",
"category": "SQLLanguageServer"
},
{
"command": "extension.fixAllFixableProblems",
"title": "Fix all auto-fixable problems",
"category": "SQLLanguageServer"
}
]
},
Expand Down
14 changes: 14 additions & 0 deletions packages/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,21 @@ export function activate(context: ExtensionContext) {
}
client.sendRequest('workspace/executeCommand', params)
})

const fixAllFixableProblem = commands.registerCommand('extension.fixAllFixableProblems', () => {
const textEditor = Window.activeTextEditor;
if (!textEditor) {
return
}
const params: ExecuteCommandParams = {
command: 'fixAllFixableProblems',
arguments: [textEditor.document.uri.toString()]
}
client.sendRequest('workspace/executeCommand', params)
})

context.subscriptions.push(switchConnection)
context.subscriptions.push(fixAllFixableProblem)
context.subscriptions.push(disposable)
client.onReady().then(() => {
client.onNotification('sqlLanguageServer.finishSetup', (params) => {
Expand Down
43 changes: 34 additions & 9 deletions packages/server/cache.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import * as fs from 'fs'
import { Diagnostic as SQLintDiagnostic } from 'sqlint'
import log4js from 'log4js'
import { Diagnostic, Range } from 'vscode-languageserver'

const logger = log4js.getLogger()

export type LintCache = { lint: SQLintDiagnostic, diagnostic: Diagnostic }
class Cache {
_cache = new Map<string, string>()
get(uri: string) {
let contents = this._cache.get(uri)
return contents
private _lintResult = new Map<string, LintCache[]>()

setLintCache(uri: string, lintCache: LintCache[]) {
this._lintResult.set(uri, lintCache)
logger.debug(this.getLintCache(uri))
}

set(uri: string, contents: string) {
this._cache.set(uri, contents)
getLintCache(uri: string): LintCache[] {
return this._lintResult.get(uri) || []
}

setFromUri(uri: string) {
this.set(uri, fs.readFileSync(uri, 'utf8'))
findLintCacheByRange(uri: string, range: Range): LintCache | null {
const lintCacheList = this.getLintCache(uri)
let minDistance = Number.MAX_VALUE
let result: LintCache | null = null
lintCacheList.filter(v => {
if (v.diagnostic.range.start.line > range.start.line || v.diagnostic.range.end.line < range.end.line) {
return false
}
if (v.diagnostic.range.start.line <= range.start.line && v.diagnostic.range.end.line > range.end.line) {
return true
}
return v.diagnostic.range.start.character <= range.start.character &&
v.diagnostic.range.end.character >= range.end.character
}).forEach(v => {
const distance = Math.abs(v.diagnostic.range.start.character - range.start.character)
if (distance < minDistance) {
minDistance = distance
result = v
}
})
return result
}
}

Expand Down
19 changes: 13 additions & 6 deletions packages/server/createDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { PublishDiagnosticsParams, Diagnostic } from 'vscode-languageserver'
import { DiagnosticSeverity }from 'vscode-languageserver-types'
import { lint, ErrorLevel } from 'sqlint'
import { LintResult } from 'sqlint/src/cli/lint'
import cache, { LintCache } from './cache'

const logger = log4js.getLogger()

function doLint(sql: string): Diagnostic[] {
function doLint(uri: string, sql: string): Diagnostic[] {
const result: LintResult[] = JSON.parse(lint({ configPath: process.cwd(), formatType: 'json', text: sql }))
const diagnostics = result.map(v => v.diagnostics).flat()
return diagnostics.map(v => {
return {
const lintDiagnostics = result.map(v => v.diagnostics).flat()
const lintCache: LintCache[] = []
const diagnostics = lintDiagnostics.map(v => {
const diagnostic = {
range: {
start: { line: v.location.start.line - 1, character: v.location.start.column - 1 },
end: { line: v.location.end.line - 1, character: v.location.end.column - 1 }
Expand All @@ -21,7 +23,11 @@ function doLint(sql: string): Diagnostic[] {
source: 'sql',
relatedInformation: []
}
lintCache.push({ diagnostic, lint: v })
return diagnostic
})
cache.setLintCache(uri, lintCache)
return diagnostics
}

export default function createDiagnostics(uri: string, sql: string): PublishDiagnosticsParams {
Expand All @@ -30,10 +36,11 @@ export default function createDiagnostics(uri: string, sql: string): PublishDiag
try {
const ast = parse(sql)
logger.debug(`ast: ${JSON.stringify(ast)}`)
diagnostics = doLint(sql)
diagnostics = doLint(uri, sql)
} catch (e) {
logger.debug('parse error')
logger.debug(e)
cache.setLintCache(uri, [])
if (e.name !== 'SyntaxError') {
throw e
}
Expand All @@ -46,7 +53,7 @@ export default function createDiagnostics(uri: string, sql: string): PublishDiag
severity: DiagnosticSeverity.Error,
// code: number | string,
source: 'sql',
relatedInformation: []
relatedInformation: [],
})
}
logger.debug(`diagnostics: ${JSON.stringify(diagnostics)}`)
Expand Down
101 changes: 87 additions & 14 deletions packages/server/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CompletionItem
} from 'vscode-languageserver'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { CodeAction, TextDocumentEdit, TextEdit, Position, CodeActionKind } from 'vscode-languageserver-types'
import cache from './cache'
import complete from './complete'
import createDiagnostics from './createDiagnostics'
Expand All @@ -15,6 +16,7 @@ import SettingStore from './SettingStore'
import { Schema } from './database_libs/AbstractClient'
import getDatabaseClient from './database_libs/getDatabaseClient'
import initializeLogging from './initializeLogging'
import { lint, LintResult } from 'sqlint'
import log4js from 'log4js'

export type ConnectionMethod = 'node-ipc' | 'stdio'
Expand All @@ -30,6 +32,12 @@ export default function createServer() {
documents.listen(connection);
let schema: Schema = []

documents.onDidChangeContent((params) => {
logger.debug(`onDidChangeContent: ${params.document.uri}, ${params.document.version}`)
const diagnostics = createDiagnostics(params.document.uri, params.document.getText())
connection.sendDiagnostics(diagnostics)
})

connection.onInitialize((params): InitializeResult => {
logger.debug(`onInitialize: ${params.rootPath}`)
SettingStore.getInstance().on('change', async () => {
Expand Down Expand Up @@ -60,34 +68,69 @@ export default function createServer() {
resolveProvider: true,
triggerCharacters: ['.'],
},
codeActionProvider: true,
executeCommandProvider: {
commands: ['sqlLanguageServer.switchDatabaseConnection']
commands: [
'sqlLanguageServer.switchDatabaseConnection',
'sqlLanguageServer.fixAllFixableProblems'
]
}
}
}
})

connection.onDidChangeTextDocument((params) => {
logger.debug(`didChangeTextDocument: ${params.textDocument.uri}`)
cache.set(params.textDocument.uri, params.contentChanges[0].text)
const diagnostics = createDiagnostics(params.textDocument.uri, params.contentChanges[0].text)
connection.sendDiagnostics(diagnostics)
})

connection.onCompletion((docParams: TextDocumentPositionParams): CompletionItem[] => {
let text = cache.get(docParams.textDocument.uri)
if (!text) {
cache.setFromUri(docParams.textDocument.uri)
text = cache.get(docParams.textDocument.uri)
}
let text = documents.get(docParams.textDocument.uri)?.getText()
if (!text) {
return []
}
logger.debug(text || '')
const candidates = complete(text || '', {
const candidates = complete(text, {
line: docParams.position.line,
column: docParams.position.character
}, schema).candidates
logger.debug(candidates.map(v => v.label).join(","))
return candidates
})

connection.onCodeAction(params => {
const lintResult = cache.findLintCacheByRange(params.textDocument.uri, params.range)
if (!lintResult) {
return []
}
const document = documents.get(params.textDocument.uri)
if (!document) {
return []
}
const text = document.getText()
if (!text) {
return []
}

function toPosition(text: string, offset: number) {
const lines = text.slice(0, offset).split('\n')
return Position.create(lines.length - 1, lines[lines.length - 1].length)
}
const fixes = Array.isArray(lintResult.lint.fix) ? lintResult.lint.fix : [lintResult.lint.fix]
if (fixes.length === 0) {
return []
}
const action = CodeAction.create(`fix: ${lintResult.diagnostic.message}`, {
documentChanges:[
TextDocumentEdit.create({ uri: params.textDocument.uri, version: document.version }, fixes.map(v => {
const edit = v.range.startOffset === v.range.endOffset
? TextEdit.insert(toPosition(text, v.range.startOffset), v.text)
: TextEdit.replace({
start: toPosition(text, v.range.startOffset),
end: toPosition(text, v.range.endOffset)
}, v.text)
return edit
}))
]
}, CodeActionKind.QuickFix)
action.diagnostics = params.context.diagnostics
return [action]
})

connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
return item
Expand All @@ -105,6 +148,36 @@ export default function createServer() {
message: e.message
})
}
} else if (request.command === 'fixAllFixableProblems') {
const uri = request.arguments ? request.arguments[0] : null
if (!uri) {
connection.sendNotification('sqlLanguageServer.error', {
message: 'fixAllFixableProblems: Need to specify uri'
})
return
}
const document = documents.get(uri)
const text = document?.getText()
if (!text) {
logger.debug('Failed to get text')
return
}
const result: LintResult[] = JSON.parse(lint({ formatType: 'json', text, fix: true }))
if (result.length === 0 && result[0].fixedText) {
logger.debug("There's no fixable problems")
return
}
logger.debug('Fix all fixable problems', text, result[0].fixedText)
connection.workspace.applyEdit({
documentChanges: [
TextDocumentEdit.create({ uri, version: document!.version }, [
TextEdit.replace({
start: Position.create(0, 0),
end: Position.create(Number.MAX_VALUE, Number.MAX_VALUE)
}, result[0].fixedText!)
])
]
})
}
})

Expand Down
3 changes: 2 additions & 1 deletion packages/server/initializeLogging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import log4js from 'log4js';
import * as path from 'path'
import * as os from 'os'
import yargs from 'yargs'

const MAX_LOG_SIZE = 1024 * 1024
const MAX_LOG_BACKUPS = 10
Expand All @@ -16,7 +17,7 @@ export default function initializeLogging() {
ackups: MAX_LOG_BACKUPS
}
},
categories: { default: { appenders: ['server'], level: 'debug' } }
categories: { default: { appenders: ['server'], level: (yargs.argv as any).debug ? 'debug' : 'debug' } }
})

const logger = log4js.getLogger()
Expand Down
1 change: 1 addition & 0 deletions packages/sql-parser/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type SelectStatement = {
where: WhereClause | null
groupBy: any
orderBy: any
location: NodeRange
}

export type FromClause = {
Expand Down
Loading

0 comments on commit afc5ec4

Please sign in to comment.