Compare commits
8 Commits
v0.22.1
...
zoom-tweak
Author | SHA1 | Date | |
---|---|---|---|
8445d2bd70 | |||
efaae2b193 | |||
7e4ebacb72 | |||
72482506c3 | |||
a51b5b09a3 | |||
53ccc1ed6c | |||
8106749ccf | |||
081e34a600 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
|
||||
|
@ -411,6 +411,47 @@ test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
|
||||
await expect(zooLogo).not.toHaveAttribute('href')
|
||||
})
|
||||
|
||||
test('if you write kcl with lint errors you get lints', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type('const my_snake_case_var = 5')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const myCamelCaseVar = 5')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// press arrows to clear autocomplete
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await page.keyboard.press('ArrowRight')
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-info')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase')
|
||||
).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('const my_snake_case_var = 5').click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('Home')
|
||||
await page.keyboard.up('Shift')
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// wait for .cm-lint-marker-info not to be visible
|
||||
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
@ -421,8 +462,8 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
/* add the following code to the editor (# error is not a valid line)
|
||||
# error
|
||||
/* add the following code to the editor ($ error is not a valid line)
|
||||
$ error
|
||||
const topAng = 30
|
||||
const bottomAng = 25
|
||||
*/
|
||||
@ -463,6 +504,8 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
await page.keyboard.type("// Let's define the same thing twice")
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const topAng = 42')
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await page.keyboard.press('ArrowRight')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible()
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
8
src-tauri/Cargo.lock
generated
8
src-tauri/Cargo.lock
generated
@ -1160,9 +1160,9 @@ checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "databake"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
|
||||
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
|
||||
dependencies = [
|
||||
"databake-derive",
|
||||
"proc-macro2",
|
||||
@ -1171,9 +1171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "databake-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -433,7 +433,6 @@ export class CameraControls {
|
||||
}
|
||||
|
||||
onMouseWheel = (event: WheelEvent) => {
|
||||
// Assume trackpad if the deltas are small and integers
|
||||
this.handleStart()
|
||||
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
@ -445,12 +444,35 @@ export class CameraControls {
|
||||
return
|
||||
}
|
||||
this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'default_camera_zoom',
|
||||
magnitude: -event.deltaY * 0.4,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_batch_req',
|
||||
batch_id: uuidv4(),
|
||||
requests: [
|
||||
{
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction: 'zoom',
|
||||
window: { x: 5, y: 5 },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
},
|
||||
{
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction: 'zoom',
|
||||
window: { x: 0, y: 5 - event.deltaY },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
},
|
||||
{
|
||||
cmd: {
|
||||
type: 'camera_drag_end',
|
||||
interaction: 'zoom',
|
||||
window: { x: 5, y: 5 - event.deltaY },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
},
|
||||
],
|
||||
responses: false,
|
||||
})
|
||||
this.handleEnd()
|
||||
return
|
||||
|
@ -71,6 +71,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
bug: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
checkmark: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -33,7 +33,7 @@ export function LowerRightControls(props: React.PropsWithChildren) {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<CustomIcon
|
||||
name="exclamationMark"
|
||||
name="bug"
|
||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||
/>
|
||||
<Tooltip position="top">Report a bug</Tooltip>
|
||||
|
@ -24,9 +24,9 @@ export function RefreshButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-10 dark:border-chalkboard-100"
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
||||
>
|
||||
<CustomIcon name="arrowRotateRight" className="w-5 h-5" />
|
||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||
<Tooltip position="bottom-right">
|
||||
<span>Refresh and report</span>
|
||||
<br />
|
||||
|
@ -11,30 +11,8 @@
|
||||
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
|
||||
--_p-block: 4px;
|
||||
--_bg: var(--chalkboard-10);
|
||||
--_shadow-alpha: 5%;
|
||||
--_shadow-alpha: 8%;
|
||||
--_theme-alpha: 0.15;
|
||||
--_theme-outline: drop-shadow(
|
||||
0 1px 0
|
||||
oklch(
|
||||
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
|
||||
var(--_theme-alpha)
|
||||
)
|
||||
)
|
||||
drop-shadow(
|
||||
0 -1px 0 oklch(var(--primary-lightness) var(--primary-chroma)
|
||||
var(--primary-hue) / var(--_theme-alpha))
|
||||
)
|
||||
drop-shadow(
|
||||
1px 0 0
|
||||
oklch(
|
||||
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
|
||||
var(--_theme-alpha)
|
||||
)
|
||||
)
|
||||
drop-shadow(
|
||||
-1px 0 0 oklch(var(--primary-lightness) var(--primary-chroma)
|
||||
var(--primary-hue) / var(--_theme-alpha))
|
||||
);
|
||||
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
@ -61,16 +39,15 @@
|
||||
background: var(--_bg);
|
||||
@apply text-chalkboard-110;
|
||||
will-change: filter;
|
||||
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 4px 8px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
var(--_theme-outline);
|
||||
filter: drop-shadow(0 1px 2px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 4px 6px hsl(0 0% 0% / calc(var(--_shadow-alpha) / 2)));
|
||||
}
|
||||
|
||||
:global(.dark) .tooltip {
|
||||
--_bg: var(--chalkboard-110);
|
||||
--_bg: var(--chalkboard-90);
|
||||
--_theme-alpha: 40%;
|
||||
--_shadow-alpha: 16%;
|
||||
@apply text-chalkboard-10;
|
||||
filter: var(--_theme-outline);
|
||||
}
|
||||
|
||||
.tooltip:dir(rtl) {
|
||||
@ -109,7 +86,7 @@
|
||||
}
|
||||
|
||||
/* Sometimes there's no visible label,
|
||||
* so we'll use the tooltip as the label
|
||||
* so we'll use the tooltip as the label
|
||||
*/
|
||||
.tooltip:only-child::before {
|
||||
content: 'Tooltip:';
|
||||
|
@ -7,7 +7,11 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight } from './highlightextension'
|
||||
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
|
||||
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
||||
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
||||
}
|
||||
|
||||
export default class EditorManager {
|
||||
private _editorView: EditorView | null = null
|
||||
@ -91,11 +95,38 @@ export default class EditorManager {
|
||||
}
|
||||
}
|
||||
|
||||
clearDiagnostics(): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
|
||||
}
|
||||
|
||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
|
||||
}
|
||||
|
||||
addDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this.editorView) return
|
||||
|
||||
forEachDiagnostic(this.editorView.state, function (diag) {
|
||||
diagnostics.push(diag)
|
||||
})
|
||||
|
||||
const uniqueDiagnostics = new Set<Diagnostic>()
|
||||
diagnostics.forEach((diagnostic) => {
|
||||
for (const knownDiagnostic of uniqueDiagnostics.values()) {
|
||||
if (diagnosticIsEqual(diagnostic, knownDiagnostic)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
uniqueDiagnostics.add(diagnostic)
|
||||
})
|
||||
|
||||
this.editorView.dispatch(
|
||||
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
|
||||
)
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this._editorView) {
|
||||
undo(this._editorView)
|
||||
|
@ -382,9 +382,14 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
case 'textDocument/publishDiagnostics':
|
||||
//const params = notification.params as PublishDiagnosticsParams
|
||||
console.log(
|
||||
'[lsp] [window/publishDiagnostics]',
|
||||
this.client.getName(),
|
||||
notification.params
|
||||
)
|
||||
const params = notification.params as PublishDiagnosticsParams
|
||||
// this is sometimes slower than our actual typing.
|
||||
//this.processDiagnostics(params)
|
||||
this.processDiagnostics(params)
|
||||
break
|
||||
case 'window/logMessage':
|
||||
console.log(
|
||||
|
@ -89,9 +89,10 @@ export class KclManager {
|
||||
return this._kclErrors
|
||||
}
|
||||
set kclErrors(kclErrors) {
|
||||
console.log('[lsp] not lsp, actually typescript: ', kclErrors)
|
||||
this._kclErrors = kclErrors
|
||||
let diagnostics = kclErrorsToDiagnostics(kclErrors)
|
||||
editorManager.setDiagnostics(diagnostics)
|
||||
editorManager.addDiagnostics(diagnostics)
|
||||
this._kclErrorsCallBack(kclErrors)
|
||||
}
|
||||
|
||||
@ -185,6 +186,11 @@ export class KclManager {
|
||||
const currentExecutionId = executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
|
||||
// here we're going to clear diagnostics since we're the first
|
||||
// one in. We're the only location where diagnostics are cleared;
|
||||
// everything from here on out should be *appending*.
|
||||
editorManager.clearDiagnostics()
|
||||
|
||||
this.isExecuting = true
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
@ -234,6 +240,7 @@ export class KclManager {
|
||||
} = { updates: 'none' }
|
||||
) {
|
||||
await this.ensureWasmInit()
|
||||
|
||||
const newCode = recast(ast)
|
||||
const newAst = this.safeParse(newCode)
|
||||
if (!newAst) return
|
||||
@ -243,6 +250,11 @@ export class KclManager {
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
this._ast = { ...newAst }
|
||||
|
||||
// here we're going to clear diagnostics since we're the first
|
||||
// one in. We're the only location where diagnostics are cleared;
|
||||
// everything from here on out should be *appending*.
|
||||
editorManager.clearDiagnostics()
|
||||
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: newAst,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
|
16
src/wasm-lib/Cargo.lock
generated
16
src/wasm-lib/Cargo.lock
generated
@ -297,9 +297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bson"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
|
||||
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.13.1",
|
||||
@ -670,9 +670,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "databake"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
|
||||
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
|
||||
dependencies = [
|
||||
"databake-derive",
|
||||
"proc-macro2",
|
||||
@ -681,9 +681,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "databake-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2037,9 +2037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -10,7 +10,7 @@ rust-version = "1.73"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
clap = "4.5.4"
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
|
@ -11,7 +11,7 @@ repository = "https://github.com/KittyCAD/modeling-app"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
databake = "0.1.7"
|
||||
databake = "0.1.8"
|
||||
kcl-lib = { path = "../kcl" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
|
@ -18,7 +18,7 @@ base64 = "0.22.1"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.4", default-features = false, optional = true }
|
||||
dashmap = "5.5.3"
|
||||
databake = { version = "0.1.7", features = ["derive"] }
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.18", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
@ -54,7 +54,7 @@ web-sys = { version = "0.3.69", features = ["console"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
approx = "0.5"
|
||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.23.0", features = ["rustls-tls-native-roots"] }
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
|
@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
use crate::{executor::SourceRange, lsp::IntoDiagnostic};
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
|
||||
#[ts(export)]
|
||||
@ -42,19 +42,9 @@ pub struct KclErrorDetails {
|
||||
}
|
||||
|
||||
impl KclError {
|
||||
/// Get the error message, line and column from the error and input code.
|
||||
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
|
||||
// Calculate the line and column of the error from the source range.
|
||||
let (line, column) = if let Some(range) = self.source_ranges().first() {
|
||||
let line = input[..range.0[0]].lines().count();
|
||||
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
|
||||
|
||||
(Some(line), Some(column))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
(format!("{}: {}", self.error_type(), self.message()), line, column)
|
||||
/// Get the error message.
|
||||
pub fn get_message(&self) -> String {
|
||||
format!("{}: {}", self.error_type(), self.message())
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> &'static str {
|
||||
@ -106,24 +96,6 @@ impl KclError {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let (message, _, _) = self.get_message_line_column(code);
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
||||
let mut new = self.clone();
|
||||
match &mut new {
|
||||
@ -163,6 +135,26 @@ impl KclError {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for KclError {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.get_message();
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is different than to_string() in that it will serialize the Error
|
||||
/// the struct as JSON so we can deserialize it on the js side.
|
||||
impl From<KclError> for String {
|
||||
|
@ -11,6 +11,7 @@ pub mod engine;
|
||||
pub mod errors;
|
||||
pub mod executor;
|
||||
pub mod fs;
|
||||
pub mod lint;
|
||||
pub mod lsp;
|
||||
pub mod parser;
|
||||
pub mod settings;
|
||||
|
64
src/wasm-lib/kcl/src/lint/ast_node.rs
Normal file
64
src/wasm-lib/kcl/src/lint/ast_node.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use crate::ast::types;
|
||||
|
||||
/// The "Node" type wraps all the AST elements we're able to find in a KCL
|
||||
/// file. Tokens we walk through will be one of these.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Node<'a> {
|
||||
Program(&'a types::Program),
|
||||
|
||||
ExpressionStatement(&'a types::ExpressionStatement),
|
||||
VariableDeclaration(&'a types::VariableDeclaration),
|
||||
ReturnStatement(&'a types::ReturnStatement),
|
||||
|
||||
VariableDeclarator(&'a types::VariableDeclarator),
|
||||
|
||||
Literal(&'a types::Literal),
|
||||
Identifier(&'a types::Identifier),
|
||||
BinaryExpression(&'a types::BinaryExpression),
|
||||
FunctionExpression(&'a types::FunctionExpression),
|
||||
CallExpression(&'a types::CallExpression),
|
||||
PipeExpression(&'a types::PipeExpression),
|
||||
PipeSubstitution(&'a types::PipeSubstitution),
|
||||
ArrayExpression(&'a types::ArrayExpression),
|
||||
ObjectExpression(&'a types::ObjectExpression),
|
||||
MemberExpression(&'a types::MemberExpression),
|
||||
UnaryExpression(&'a types::UnaryExpression),
|
||||
|
||||
Parameter(&'a types::Parameter),
|
||||
|
||||
ObjectProperty(&'a types::ObjectProperty),
|
||||
|
||||
MemberObject(&'a types::MemberObject),
|
||||
LiteralIdentifier(&'a types::LiteralIdentifier),
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($node:ident, $t: ident) => {
|
||||
impl<'a> From<&'a types::$t> for Node<'a> {
|
||||
fn from(v: &'a types::$t) -> Self {
|
||||
Node::$t(v)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from!(Node, Program);
|
||||
impl_from!(Node, ExpressionStatement);
|
||||
impl_from!(Node, VariableDeclaration);
|
||||
impl_from!(Node, ReturnStatement);
|
||||
impl_from!(Node, VariableDeclarator);
|
||||
impl_from!(Node, Literal);
|
||||
impl_from!(Node, Identifier);
|
||||
impl_from!(Node, BinaryExpression);
|
||||
impl_from!(Node, FunctionExpression);
|
||||
impl_from!(Node, CallExpression);
|
||||
impl_from!(Node, PipeExpression);
|
||||
impl_from!(Node, PipeSubstitution);
|
||||
impl_from!(Node, ArrayExpression);
|
||||
impl_from!(Node, ObjectExpression);
|
||||
impl_from!(Node, MemberExpression);
|
||||
impl_from!(Node, UnaryExpression);
|
||||
impl_from!(Node, Parameter);
|
||||
impl_from!(Node, ObjectProperty);
|
||||
impl_from!(Node, MemberObject);
|
||||
impl_from!(Node, LiteralIdentifier);
|
233
src/wasm-lib/kcl/src/lint/ast_walk.rs
Normal file
233
src/wasm-lib/kcl/src/lint/ast_walk.rs
Normal file
@ -0,0 +1,233 @@
|
||||
use super::Node;
|
||||
use crate::ast::types::{
|
||||
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Walker is implemented by things that are able to walk an AST tree to
|
||||
/// produce lints. This trait is implemented automatically for a few of the
|
||||
/// common types, but can be manually implemented too.
|
||||
pub trait Walker<'a> {
|
||||
/// Walk will visit every element of the AST.
|
||||
fn walk(&self, n: Node<'a>) -> Result<bool>;
|
||||
}
|
||||
|
||||
impl<'a, FnT> Walker<'a> for FnT
|
||||
where
|
||||
FnT: Fn(Node<'a>) -> Result<bool>,
|
||||
{
|
||||
fn walk(&self, n: Node<'a>) -> Result<bool> {
|
||||
self(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the Walker against all [Node]s in a [Program].
|
||||
pub fn walk<'a, WalkT>(prog: &'a Program, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(prog.into())?;
|
||||
|
||||
for bi in &prog.body {
|
||||
walk_body_item(bi, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_variable_declarator<'a, WalkT>(node: &'a VariableDeclarator, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
f.walk((&node.id).into())?;
|
||||
walk_value(&node.init, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
f.walk((&node.identifier).into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_member_expression<'a, WalkT>(node: &'a MemberExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
|
||||
walk_member_object(&node.object, f)?;
|
||||
walk_literal_identifier(&node.property, f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
match node {
|
||||
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into())?,
|
||||
BinaryPart::Identifier(id) => f.walk(id.as_ref().into())?,
|
||||
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into())?,
|
||||
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into())?,
|
||||
BinaryPart::UnaryExpression(ue) => {
|
||||
walk_unary_expression(ue, f)?;
|
||||
true
|
||||
}
|
||||
BinaryPart::MemberExpression(me) => {
|
||||
walk_member_expression(me, f)?;
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_value<'a, WalkT>(node: &'a Value, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
match node {
|
||||
Value::Literal(lit) => {
|
||||
f.walk(lit.as_ref().into())?;
|
||||
}
|
||||
|
||||
Value::Identifier(id) => {
|
||||
// sometimes there's a bare Identifier without a Value::Identifier.
|
||||
f.walk(id.as_ref().into())?;
|
||||
}
|
||||
|
||||
Value::BinaryExpression(be) => {
|
||||
f.walk(be.as_ref().into())?;
|
||||
|
||||
walk_binary_part(&be.left, f)?;
|
||||
walk_binary_part(&be.right, f)?;
|
||||
}
|
||||
Value::FunctionExpression(fe) => {
|
||||
f.walk(fe.as_ref().into())?;
|
||||
|
||||
for arg in &fe.params {
|
||||
walk_parameter(arg, f)?;
|
||||
}
|
||||
walk(&fe.body, f)?;
|
||||
}
|
||||
Value::CallExpression(ce) => {
|
||||
f.walk(ce.as_ref().into())?;
|
||||
f.walk((&ce.callee).into())?;
|
||||
for e in &ce.arguments {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::PipeExpression(pe) => {
|
||||
f.walk(pe.as_ref().into())?;
|
||||
|
||||
for e in &pe.body {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::PipeSubstitution(ps) => {
|
||||
f.walk(ps.as_ref().into())?;
|
||||
}
|
||||
Value::ArrayExpression(ae) => {
|
||||
f.walk(ae.as_ref().into())?;
|
||||
for e in &ae.elements {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::ObjectExpression(oe) => {
|
||||
walk_object_expression(oe, f)?;
|
||||
}
|
||||
Value::MemberExpression(me) => {
|
||||
walk_member_expression(me, f)?;
|
||||
}
|
||||
Value::UnaryExpression(ue) => {
|
||||
walk_unary_expression(ue, f)?;
|
||||
}
|
||||
_ => {
|
||||
println!("{:?}", node);
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk through an [ObjectProperty].
|
||||
fn walk_object_property<'a, WalkT>(node: &'a ObjectProperty, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
walk_value(&node.value, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk through an [ObjectExpression].
|
||||
fn walk_object_expression<'a, WalkT>(node: &'a ObjectExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
for prop in &node.properties {
|
||||
walk_object_property(prop, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// walk through an [UnaryExpression].
|
||||
fn walk_unary_expression<'a, WalkT>(node: &'a UnaryExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
walk_binary_part(&node.argument, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// walk through a [BodyItem].
|
||||
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
// We don't walk a BodyItem since it's an enum itself.
|
||||
|
||||
match node {
|
||||
BodyItem::ExpressionStatement(xs) => {
|
||||
f.walk(xs.into())?;
|
||||
walk_value(&xs.expression, f)?;
|
||||
}
|
||||
BodyItem::VariableDeclaration(vd) => {
|
||||
f.walk(vd.into())?;
|
||||
for dec in &vd.declarations {
|
||||
walk_variable_declarator(dec, f)?;
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(rs) => {
|
||||
f.walk(rs.into())?;
|
||||
walk_value(&rs.argument, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
131
src/wasm-lib/kcl/src/lint/checks/camel_case.rs
Normal file
131
src/wasm-lib/kcl/src/lint/checks/camel_case.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use crate::{
|
||||
ast::types::VariableDeclarator,
|
||||
executor::SourceRange,
|
||||
lint::{
|
||||
rule::{def_finding, Discovered, Finding},
|
||||
Node,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
def_finding!(
|
||||
Z0001,
|
||||
"Identifiers must be lowerCamelCase",
|
||||
"\
|
||||
By convention, variable names are lowerCamelCase, not snake_case, kebab-case,
|
||||
nor CammelCase. 🐪
|
||||
|
||||
For instance, a good identifier for the variable representing 'box height'
|
||||
would be 'boxHeight', not 'BOX_HEIGHT', 'box_height' nor 'BoxHeight'. For
|
||||
more information there's a pretty good Wikipedia page at
|
||||
|
||||
https://en.wikipedia.org/wiki/Camel_case
|
||||
"
|
||||
);
|
||||
|
||||
fn lint_lower_camel_case(decl: &VariableDeclarator) -> Result<Vec<Discovered>> {
|
||||
let mut findings = vec![];
|
||||
let ident = &decl.id;
|
||||
let name = &ident.name;
|
||||
|
||||
if !name.chars().next().unwrap().is_lowercase() {
|
||||
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
|
||||
return Ok(findings);
|
||||
}
|
||||
|
||||
if name.contains('-') || name.contains('_') {
|
||||
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
|
||||
return Ok(findings);
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
pub fn lint_variables(decl: Node) -> Result<Vec<Discovered>> {
|
||||
let Node::VariableDeclaration(decl) = decl else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
Ok(decl
|
||||
.declarations
|
||||
.iter()
|
||||
.flat_map(|v| lint_lower_camel_case(v).unwrap_or_default())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{lint_variables, Z0001};
|
||||
use crate::lint::rule::{assert_finding, test_finding, test_no_finding};
|
||||
|
||||
#[test]
|
||||
fn z0001_const() {
|
||||
assert_finding!(lint_variables, Z0001, "const Thickness = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const THICKNESS = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const THICC_NES = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const thicc_nes = 0.5");
|
||||
}
|
||||
|
||||
test_finding!(z0001_full_bad, lint_variables, Z0001, "\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
const pipeLargeDia = 20
|
||||
const thickness = 0.5
|
||||
|
||||
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
|
||||
const Part001 = startSketchOn('XY')
|
||||
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|
||||
|> line([thickness, 0], %)
|
||||
|> line([0, -1], %)
|
||||
|> angledLineToX({
|
||||
angle: 60,
|
||||
to: pipeSmallDia + thickness
|
||||
}, %)
|
||||
|> line([0, -pipeLength], %)
|
||||
|> angledLineToX({
|
||||
angle: -60,
|
||||
to: pipeLargeDia + thickness
|
||||
}, %)
|
||||
|> line([0, -1], %)
|
||||
|> line([-thickness, 0], %)
|
||||
|> line([0, 1], %)
|
||||
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|
||||
|> line([0, pipeLength], %)
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
|
||||
test_no_finding!(z0001_full_good, lint_variables, Z0001, "\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
const pipeLargeDia = 20
|
||||
const thickness = 0.5
|
||||
|
||||
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
|
||||
const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|
||||
|> line([thickness, 0], %)
|
||||
|> line([0, -1], %)
|
||||
|> angledLineToX({
|
||||
angle: 60,
|
||||
to: pipeSmallDia + thickness
|
||||
}, %)
|
||||
|> line([0, -pipeLength], %)
|
||||
|> angledLineToX({
|
||||
angle: -60,
|
||||
to: pipeLargeDia + thickness
|
||||
}, %)
|
||||
|> line([0, -1], %)
|
||||
|> line([-thickness, 0], %)
|
||||
|> line([0, 1], %)
|
||||
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|
||||
|> line([0, pipeLength], %)
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
}
|
4
src/wasm-lib/kcl/src/lint/checks/mod.rs
Normal file
4
src/wasm-lib/kcl/src/lint/checks/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod camel_case;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use camel_case::{lint_variables, Z0001};
|
9
src/wasm-lib/kcl/src/lint/mod.rs
Normal file
9
src/wasm-lib/kcl/src/lint/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod ast_node;
|
||||
mod ast_walk;
|
||||
pub mod checks;
|
||||
mod rule;
|
||||
|
||||
pub use ast_node::Node;
|
||||
pub use ast_walk::walk;
|
||||
// pub(crate) use rule::{def_finding, finding};
|
||||
pub use rule::{lint, Discovered, Finding};
|
180
src/wasm-lib/kcl/src/lint/rule.rs
Normal file
180
src/wasm-lib/kcl/src/lint/rule.rs
Normal file
@ -0,0 +1,180 @@
|
||||
use super::{walk, Node};
|
||||
use crate::{ast::types::Program, executor::SourceRange, lsp::IntoDiagnostic};
|
||||
use anyhow::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
/// Check the provided AST for any found rule violations.
|
||||
///
|
||||
/// The Rule trait is automatically implemented for a few other types,
|
||||
/// but it can also be manually implemented as required.
|
||||
pub trait Rule<'a> {
|
||||
/// Check the AST at this specific node for any Finding(s).
|
||||
fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
|
||||
}
|
||||
|
||||
impl<'a, FnT> Rule<'a> for FnT
|
||||
where
|
||||
FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
|
||||
{
|
||||
fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
|
||||
self(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Specific discovered lint rule Violation of a particular Finding.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Discovered {
|
||||
/// Zoo Lint Finding information.
|
||||
pub finding: Finding,
|
||||
|
||||
/// Further information about the specific finding.
|
||||
pub description: String,
|
||||
|
||||
/// Source code location.
|
||||
pub pos: SourceRange,
|
||||
|
||||
/// Is this discovered issue overridden by the programmer?
|
||||
pub overridden: bool,
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for Discovered {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.finding.title.to_owned();
|
||||
let source_range = self.pos;
|
||||
|
||||
Diagnostic {
|
||||
range: source_range.to_lsp_range(code),
|
||||
severity: Some(DiagnosticSeverity::INFORMATION),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("lint".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract lint problem type.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Finding {
|
||||
/// Unique identifier for this particular issue.
|
||||
pub code: &'static str,
|
||||
|
||||
/// Short one-line description of this issue.
|
||||
pub title: &'static str,
|
||||
|
||||
/// Long human-readable description of this issue.
|
||||
pub description: &'static str,
|
||||
|
||||
/// Is this discovered issue experimental?
|
||||
pub experimental: bool,
|
||||
}
|
||||
|
||||
impl Finding {
|
||||
/// Create a new Discovered finding at the specific Position.
|
||||
pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
|
||||
Discovered {
|
||||
description,
|
||||
finding: self.clone(),
|
||||
pos,
|
||||
overridden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! def_finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
/// Generated Finding
|
||||
pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
|
||||
};
|
||||
}
|
||||
pub(crate) use def_finding;
|
||||
|
||||
macro_rules! finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
$crate::lint::rule::Finding {
|
||||
code: stringify!($code),
|
||||
title: $title,
|
||||
description: $description,
|
||||
experimental: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use finding;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
|
||||
|
||||
/// Check the provided Program for any Findings.
|
||||
pub fn lint<'a, RuleT>(prog: &'a Program, rule: RuleT) -> Result<Vec<Discovered>>
|
||||
where
|
||||
RuleT: Rule<'a>,
|
||||
{
|
||||
let v = Arc::new(Mutex::new(vec![]));
|
||||
walk(prog, &|node: Node<'a>| {
|
||||
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||
findings.append(&mut rule.check(node)?);
|
||||
Ok(true)
|
||||
})?;
|
||||
let x = v.lock().unwrap();
|
||||
Ok(x.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
macro_rules! assert_no_finding {
|
||||
( $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
assert!(false, "Finding {:?} was emitted", $finding.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_finding {
|
||||
( $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
return;
|
||||
}
|
||||
}
|
||||
assert!(false, "Finding {:?} was not emitted", $finding.code);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_finding {
|
||||
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$crate::lint::rule::assert_finding!($check, $finding, $kcl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_no_finding {
|
||||
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use assert_finding;
|
||||
pub(crate) use assert_no_finding;
|
||||
pub(crate) use test_finding;
|
||||
pub(crate) use test_no_finding;
|
||||
}
|
@ -36,9 +36,9 @@ use tower_lsp::{
|
||||
use super::backend::{InnerHandle, UpdateHandle};
|
||||
use crate::{
|
||||
ast::types::VariableKind,
|
||||
errors::KclError,
|
||||
executor::SourceRange,
|
||||
lsp::{backend::Backend as _, safemap::SafeMap},
|
||||
lint::{checks, lint},
|
||||
lsp::{backend::Backend as _, safemap::SafeMap, util::IntoDiagnostic},
|
||||
parser::PIPE_OPERATOR,
|
||||
};
|
||||
|
||||
@ -166,6 +166,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
}
|
||||
|
||||
async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
|
||||
self.clear_diagnostics_map(¶ms.uri).await;
|
||||
// We already updated the code map in the shared backend.
|
||||
|
||||
// Lets update the tokens.
|
||||
@ -251,14 +252,14 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
// Execute the code if we have an executor context.
|
||||
// This function automatically executes if we should & updates the diagnostics if we got
|
||||
// errors.
|
||||
let result = self.execute(¶ms, ast).await;
|
||||
if result.is_err() {
|
||||
// We return early because we got errors, and we don't want to clear the diagnostics.
|
||||
if self.execute(¶ms, ast.clone()).await.is_err() {
|
||||
// if there was an issue, let's bail and avoid trying to lint.
|
||||
return;
|
||||
}
|
||||
|
||||
// Lets update the diagnostics, since we got no errors.
|
||||
self.clear_diagnostics(¶ms.uri).await;
|
||||
for discovered_finding in lint(&ast, checks::lint_variables).into_iter().flatten() {
|
||||
self.add_to_diagnostics(¶ms, discovered_finding).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,30 +357,7 @@ impl Backend {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn add_to_diagnostics(&self, params: &TextDocumentItem, err: KclError) {
|
||||
let diagnostic = err.to_lsp_diagnostic(¶ms.text);
|
||||
// We got errors, update the diagnostics.
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
params.uri.to_string(),
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![diagnostic.clone()],
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Publish the diagnostic.
|
||||
// If the client supports it.
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), vec![diagnostic], None)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn clear_diagnostics(&self, uri: &url::Url) {
|
||||
async fn clear_diagnostics_map(&self, uri: &url::Url) {
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
uri.to_string(),
|
||||
@ -392,10 +370,43 @@ impl Backend {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Publish the diagnostic, we reset it here so the client knows the code compiles now.
|
||||
// If the client supports it.
|
||||
self.client.publish_diagnostics(uri.clone(), vec![], None).await;
|
||||
async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
|
||||
&self,
|
||||
params: &TextDocumentItem,
|
||||
diagnostic: DiagT,
|
||||
) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostic))
|
||||
.await;
|
||||
|
||||
let diagnostic = diagnostic.to_lsp_diagnostic(¶ms.text);
|
||||
|
||||
let DocumentDiagnosticReport::Full(mut report) = self
|
||||
.diagnostics_map
|
||||
.get(params.uri.clone().as_str())
|
||||
.await
|
||||
.unwrap_or(DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![],
|
||||
},
|
||||
}))
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
report.full_document_diagnostic_report.items.push(diagnostic);
|
||||
|
||||
self.diagnostics_map
|
||||
.insert(params.uri.to_string(), DocumentDiagnosticReport::Full(report.clone()))
|
||||
.await;
|
||||
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), report.full_document_diagnostic_report.items, None)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn execute(&self, params: &TextDocumentItem, ast: crate::ast::types::Program) -> Result<()> {
|
||||
|
@ -7,3 +7,5 @@ mod safemap;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod util;
|
||||
|
||||
pub use util::IntoDiagnostic;
|
||||
|
@ -1498,6 +1498,53 @@ async fn test_kcl_lsp_diagnostic_has_errors() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_kcl_lsp_diagnostic_has_lints() {
|
||||
let server = kcl_lsp_server(false).await.unwrap();
|
||||
|
||||
// Send open file.
|
||||
server
|
||||
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentItem {
|
||||
uri: "file:///testlint.kcl".try_into().unwrap(),
|
||||
language_id: "kcl".to_string(),
|
||||
version: 1,
|
||||
text: r#"let THING = 10"#.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
server.wait_on_handle().await;
|
||||
|
||||
// Send diagnostics request.
|
||||
let diagnostics = server
|
||||
.diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///testlint.kcl".try_into().unwrap(),
|
||||
},
|
||||
partial_result_params: Default::default(),
|
||||
work_done_progress_params: Default::default(),
|
||||
identifier: None,
|
||||
previous_result_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check the diagnostics.
|
||||
if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics {
|
||||
if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics {
|
||||
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
|
||||
assert_eq!(
|
||||
diagnostics.full_document_diagnostic_report.items[0].message,
|
||||
"Identifiers must be lowerCamelCase"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected full diagnostics");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected diagnostics");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_copilot_lsp_set_editor_info() {
|
||||
let server = copilot_lsp_server().await.unwrap();
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Utility functions for working with ropes and positions.
|
||||
|
||||
use ropey::Rope;
|
||||
use tower_lsp::lsp_types::Position;
|
||||
use tower_lsp::lsp_types::{Diagnostic, Position};
|
||||
|
||||
pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
|
||||
Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize)
|
||||
@ -31,3 +31,10 @@ pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
|
||||
let line_start = offset - char_offset;
|
||||
Some(rope.slice(line_start..offset).to_string())
|
||||
}
|
||||
|
||||
/// Convert an object into a [lsp_types::Diagnostic] given the
|
||||
/// [TextDocumentItem]'s `.text` field.
|
||||
pub trait IntoDiagnostic {
|
||||
/// Convert the traited object to a [lsp_types::Diagnostic].
|
||||
fn to_lsp_diagnostic(&self, text: &str) -> Diagnostic;
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ pub async fn min(args: Args) -> Result<MemoryItem, KclError> {
|
||||
tags = ["math"],
|
||||
}]
|
||||
fn inner_min(args: Vec<f64>) -> f64 {
|
||||
let mut min = std::f64::MAX;
|
||||
let mut min = f64::MAX;
|
||||
for arg in args.iter() {
|
||||
if *arg < min {
|
||||
min = *arg;
|
||||
@ -312,7 +312,7 @@ pub async fn max(args: Args) -> Result<MemoryItem, KclError> {
|
||||
tags = ["math"],
|
||||
}]
|
||||
fn inner_max(args: Vec<f64>) -> f64 {
|
||||
let mut max = std::f64::MIN;
|
||||
let mut max = f64::MIN;
|
||||
for arg in args.iter() {
|
||||
if *arg > max {
|
||||
max = *arg;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 102 KiB |
Reference in New Issue
Block a user