Skip to content

Commit fb7dbb5

Browse files
committed
feat(linters): run compiler and Misti checks on files
Fixes #177 Fixes #22
1 parent 9dc58e3 commit fb7dbb5

File tree

10 files changed

+230
-218
lines changed

10 files changed

+230
-218
lines changed

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,16 @@
319319
],
320320
"default": "workspace",
321321
"description": "Where to search when using Find Usages"
322+
},
323+
"tact.linters.misti.enable": {
324+
"type": "boolean",
325+
"default": true,
326+
"description": "Enable Misti static analyzer"
327+
},
328+
"tact.linters.misti.binPath": {
329+
"type": "string",
330+
"default": "npx misti",
331+
"description": "Path to Misti binary/command to run Misti"
322332
}
323333
}
324334
},

server/src/compiler/MistiAnalyzer.ts

+44-116
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,54 @@
11
import * as cp from "node:child_process"
2+
import {CompilerError, Severity, TactCompiler} from "@server/compiler/TactCompiler"
3+
import {getDocumentSettings} from "@server/utils/settings"
24

3-
export enum Severity {
4-
INFO = 1,
5-
LOW = 2,
6-
MEDIUM = 3,
7-
HIGH = 4,
8-
CRITICAL = 5,
5+
export interface MistiJsonOutput {
6+
readonly kind: "warnings"
7+
readonly warnings: MistiProjectWarning[]
98
}
109

11-
export interface CompilerError {
12-
severity: Severity
13-
line: number
14-
character: number
15-
message: string
16-
file: string
17-
length?: number
10+
export interface MistiProjectWarning {
11+
readonly projectName?: string
12+
readonly warnings: string[]
1813
}
1914

20-
interface MistiJsonOutput {
21-
kind: "warnings"
22-
warnings: MistiProjectWarning[]
23-
}
24-
25-
interface MistiProjectWarning {
26-
projectName?: string
27-
warnings: string[]
28-
}
29-
30-
interface MistiWarning {
31-
file: string
32-
line: number | string
33-
col: number | string
34-
detectorId?: string
35-
severity: string
36-
message: string
15+
export interface MistiWarning {
16+
readonly file: string
17+
readonly line: number | string
18+
readonly col: number | string
19+
readonly detectorId?: string
20+
readonly severity: string
21+
readonly message: string
3722
}
3823

3924
export class MistiAnalyzer {
4025
private static parseCompilerOutput(output: string): CompilerError[] {
4126
const errors: CompilerError[] = []
4227
const jsonStart = output.indexOf("{")
43-
if (jsonStart === -1) {
44-
return MistiAnalyzer.parseTactCompilerOutput(output)
28+
const jsonEnd = output.lastIndexOf("}")
29+
if (jsonStart === -1 || jsonEnd === -1) {
30+
return TactCompiler.parseCompilerOutput(output)
4531
}
4632

47-
const jsonString = output.slice(jsonStart)
33+
const jsonString = output.slice(jsonStart, jsonEnd + 1)
4834
try {
4935
const jsonData = JSON.parse(jsonString) as MistiJsonOutput
5036
for (const projectWarning of jsonData.warnings) {
5137
if (!Array.isArray(projectWarning.warnings)) continue
5238

53-
for (const warningStr of projectWarning.warnings) {
39+
for (const warningJSON of projectWarning.warnings) {
5440
try {
55-
const warning = JSON.parse(warningStr) as MistiWarning
56-
const errorObj: CompilerError = {
57-
file: warning.file,
41+
const warning = JSON.parse(warningJSON) as MistiWarning
42+
errors.push({
43+
file: warning.file.trim(),
5844
line: Number(warning.line) - 1,
5945
character: Number(warning.col) - 1,
60-
message: `[${warning.severity.toUpperCase()}] ${warning.detectorId ? warning.detectorId + ": " : ""}${warning.message}`,
46+
message: `[${warning.severity.toUpperCase()}] ${warning.message}`,
47+
id: warning.detectorId ?? "",
6148
severity: MistiAnalyzer.mapSeverity(warning.severity),
62-
}
63-
errors.push(errorObj)
64-
console.info(
65-
`[MistiAnalyzer] Parsed warning from JSON: ${JSON.stringify(errorObj)}`,
66-
)
49+
})
6750
} catch {
68-
console.error(`Failed to parse internal warning: ${warningStr}`)
51+
console.error(`Failed to parse internal warning: ${warningJSON}`)
6952
}
7053
}
7154
}
@@ -75,85 +58,30 @@ export class MistiAnalyzer {
7558
console.error(`Failed to parse JSON output: ${error}`)
7659
}
7760

78-
return MistiAnalyzer.parseTactCompilerOutput(output)
79-
}
80-
81-
private static parseTactCompilerOutput(output: string): CompilerError[] {
82-
const errors: CompilerError[] = []
83-
const lines = output.split("\n")
84-
for (let i = 0; i < lines.length; i++) {
85-
const line = lines[i]
86-
const match =
87-
/^(Compilation error:|Syntax error:|Error:)\s*([^:]+):(\d+):(\d+):\s*(.+)$/.exec(
88-
line,
89-
)
90-
if (!match) continue
91-
const prefix = match[1]
92-
const file = match[2]
93-
const lineNum = match[3]
94-
const char = match[4]
95-
const rawMessage = match[5]
96-
let fullMessage = `${prefix} ${file}:${lineNum}:${char}: ${rawMessage}\n`
97-
let contextFound = false
98-
for (let j = i + 1; j < lines.length; j++) {
99-
const nextLine = lines[j]
100-
if (
101-
nextLine.startsWith("Compilation error:") ||
102-
nextLine.startsWith("Syntax error:") ||
103-
nextLine.startsWith("Error:")
104-
)
105-
break
106-
if (nextLine.includes("Line") || nextLine.includes("|") || nextLine.includes("^")) {
107-
contextFound = true
108-
fullMessage += nextLine + "\n"
109-
i = j
110-
}
111-
}
112-
const error: CompilerError = {
113-
file,
114-
line: Number.parseInt(lineNum, 10) - 1,
115-
character: Number.parseInt(char, 10) - 1,
116-
message: fullMessage.trim(),
117-
severity: Severity.HIGH,
118-
}
119-
120-
if (contextFound) {
121-
const caretLine = fullMessage.split("\n").find(l => l.includes("^"))
122-
if (caretLine) error.length = caretLine.trim().length
123-
}
124-
errors.push(error)
125-
console.info(`[MistiAnalyzer] Parsed error: ${JSON.stringify(error)}`)
126-
}
127-
return errors
61+
return TactCompiler.parseCompilerOutput(output)
12862
}
12963

13064
private static mapSeverity(sev: string): Severity {
131-
switch (sev.toUpperCase()) {
132-
case "INFO": {
133-
return Severity.INFO
134-
}
135-
case "LOW": {
136-
return Severity.LOW
137-
}
138-
case "MEDIUM": {
139-
return Severity.MEDIUM
140-
}
141-
case "HIGH": {
142-
return Severity.HIGH
143-
}
144-
case "CRITICAL": {
145-
return Severity.CRITICAL
146-
}
147-
default: {
148-
return Severity.HIGH
149-
}
150-
}
65+
const s = sev.toUpperCase()
66+
if (s === "INFO") return Severity.INFO
67+
if (s === "LOW") return Severity.LOW
68+
if (s === "MEDIUM") return Severity.MEDIUM
69+
if (s === "HIGH") return Severity.HIGH
70+
if (s === "CRITICAL") return Severity.CRITICAL
71+
return Severity.HIGH
15172
}
15273

153-
public static async analyze(_filePath: string): Promise<CompilerError[]> {
74+
public static async analyze(filePath: string): Promise<CompilerError[]> {
75+
// if (existsSync("./tact.config.json")) {
76+
// console.warn("Tact config not found")
77+
// return []
78+
// }
79+
80+
const settings = await getDocumentSettings(`file://${filePath}`)
81+
15482
return new Promise((resolve, reject) => {
15583
const process = cp.exec(
156-
`npx misti ./tact.config.json --output-format json`,
84+
`${settings.linters.misti.binPath} ./tact.config.json --output-format json`,
15785
(_error, stdout, stderr) => {
15886
const output = stdout + "\n" + stderr
15987
const errors = this.parseCompilerOutput(output)

server/src/compiler/TactCompiler.ts

+49-33
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,81 @@
11
import * as cp from "node:child_process"
2-
import * as path from "node:path"
2+
import {toolchain} from "@server/toolchain"
33

4-
interface CompilerError {
5-
line: number
6-
character: number
7-
message: string
8-
file: string
9-
length?: number
4+
export enum Severity {
5+
INFO = 1,
6+
LOW = 2,
7+
MEDIUM = 3,
8+
HIGH = 4,
9+
CRITICAL = 5,
10+
}
11+
12+
export interface CompilerError {
13+
readonly line: number
14+
readonly character: number
15+
readonly message: string
16+
readonly file: string
17+
readonly length?: number
18+
readonly id: string
19+
readonly severity: Severity
1020
}
1121

1222
export class TactCompiler {
13-
private static parseCompilerOutput(output: string): CompilerError[] {
23+
public static parseCompilerOutput(output: string): CompilerError[] {
1424
const errors: CompilerError[] = []
1525
const lines = output.split("\n")
1626

1727
for (let i = 0; i < lines.length; i++) {
1828
const line = lines[i]
19-
const match = /^Error: ([^:]+):(\d+):(\d+): (.+)$/.exec(line)
29+
const match =
30+
/^(Compilation error:|Syntax error:|Error:)([^:]+):(\d+):(\d+): (.+)$/.exec(line)
2031
if (!match) continue
2132

22-
console.info(`[TactCompiler] Found error line: ${line}`)
23-
const [, file, lineNum, char, rawMessage] = match
24-
let fullMessage = `${file}:${lineNum}:${char}: ${rawMessage}\n`
25-
let contextFound = false
33+
const [, _, file, lineNum, char, message] = match
34+
35+
let length = 0
2636

2737
for (let j = i + 1; j < lines.length; j++) {
2838
const nextLine = lines[j]
29-
if (nextLine.startsWith("Error:")) break
39+
if (
40+
nextLine.startsWith("Compilation error:") ||
41+
nextLine.startsWith("Syntax error:") ||
42+
nextLine.startsWith("Error:")
43+
) {
44+
break
45+
}
3046
if (nextLine.includes("Line") || nextLine.includes("|") || nextLine.includes("^")) {
31-
contextFound = true
32-
fullMessage += nextLine + "\n"
3347
i = j
3448
}
49+
50+
if (nextLine.includes("^")) {
51+
length = nextLine.trim().length
52+
}
3553
}
3654

37-
const error: CompilerError = {
38-
file,
55+
errors.push({
56+
file: file.trim(),
3957
line: Number.parseInt(lineNum, 10) - 1,
4058
character: Number.parseInt(char, 10) - 1,
41-
message: contextFound ? fullMessage.trim() : rawMessage,
42-
}
43-
44-
if (contextFound) {
45-
const caretLine = fullMessage.split("\n").find(l => l.includes("^"))
46-
if (caretLine) error.length = caretLine.trim().length
47-
}
48-
49-
errors.push(error)
50-
console.info(`[TactCompiler] Parsed error: ${JSON.stringify(error)}`)
59+
message,
60+
length,
61+
id: "",
62+
severity: Severity.CRITICAL,
63+
} satisfies CompilerError)
5164
}
5265

5366
return errors
5467
}
5568

56-
public static async compile(_filePath: string): Promise<CompilerError[]> {
57-
return new Promise((resolve, reject) => {
58-
const tactPath = path.join(__dirname, "../node_modules/.bin/tact")
69+
public static async checkProject(): Promise<CompilerError[]> {
70+
// if (existsSync("./tact.config.json")) {
71+
// console.warn("Tact config not found")
72+
// return []
73+
// }
5974

75+
return new Promise((resolve, reject) => {
6076
const process = cp.exec(
61-
`${tactPath} --check --config ./tact.config.json`,
62-
(_1, _2, stderr) => {
77+
`${toolchain.compilerPath} --check --config ./tact.config.json`,
78+
(_error, _stdout, stderr) => {
6379
const errors = this.parseCompilerOutput(stderr)
6480
resolve(errors)
6581
},

server/src/inspections/CompilerInspection.ts

+19-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as lsp from "vscode-languageserver"
22
import type {File} from "@server/psi/File"
33
import {TactCompiler} from "@server/compiler/TactCompiler"
4-
import {URI} from "vscode-uri"
54
import {Inspection, InspectionIds} from "./Inspection"
5+
import {URI} from "vscode-uri"
66

77
export class CompilerInspection implements Inspection {
88
public readonly id: "tact-compiler-errors" = InspectionIds.COMPILER
@@ -12,24 +12,26 @@ export class CompilerInspection implements Inspection {
1212

1313
try {
1414
const filePath = URI.parse(file.uri).fsPath
15-
const errors = await TactCompiler.compile(filePath)
15+
const errors = await TactCompiler.checkProject()
1616

17-
return errors.map(error => ({
18-
severity: lsp.DiagnosticSeverity.Error,
19-
range: {
20-
start: {
21-
line: error.line,
22-
character: error.character,
23-
},
24-
end: {
25-
line: error.line,
26-
character: error.character + (error.length ?? 1),
17+
return errors
18+
.filter(error => filePath.endsWith(error.file))
19+
.map(error => ({
20+
severity: lsp.DiagnosticSeverity.Error,
21+
range: {
22+
start: {
23+
line: error.line,
24+
character: error.character,
25+
},
26+
end: {
27+
line: error.line,
28+
character: error.character + (error.length ?? 1),
29+
},
2730
},
28-
},
29-
message: error.message,
30-
source: "tact-compiler",
31-
code: this.id,
32-
}))
31+
message: error.message,
32+
source: "tact-compiler",
33+
code: this.id,
34+
}))
3335
} catch {
3436
return []
3537
}

server/src/inspections/Inspection.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const InspectionIds = {
1111
UNUSED_IMPORT: "unused-import",
1212
MISSED_FIELD_IN_CONTRACT: "missed-field-in-contract",
1313
NOT_IMPORTED_SYMBOL: "not-imported-symbol",
14+
MISTI: "misti",
1415
} as const
1516

1617
export type InspectionId = (typeof InspectionIds)[keyof typeof InspectionIds]

0 commit comments

Comments
 (0)