@herb-tools/language-server 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,15 @@
1
1
  import { TextDocument } from "vscode-languageserver-textdocument"
2
- import { Connection, Diagnostic } from "vscode-languageserver/node"
2
+ import { Connection, Diagnostic, DiagnosticSeverity, DiagnosticTag } from "vscode-languageserver/node"
3
+ import { Visitor } from "@herb-tools/core"
4
+
5
+ import type {
6
+ Node,
7
+ ERBCaseNode,
8
+ ERBCaseMatchNode,
9
+ HTMLTextNode,
10
+ } from "@herb-tools/core"
11
+
12
+ import { isHTMLTextNode } from "@herb-tools/core"
3
13
 
4
14
  import { ParserService } from "./parser_service"
5
15
  import { LinterService } from "./linter_service"
@@ -36,10 +46,12 @@ export class Diagnostics {
36
46
  } else {
37
47
  const parseResult = this.parserService.parseDocument(textDocument)
38
48
  const lintResult = await this.linterService.lintDocument(textDocument)
49
+ const unreachableCodeDiagnostics = this.getUnreachableCodeDiagnostics(parseResult.document)
39
50
 
40
51
  allDiagnostics = [
41
52
  ...parseResult.diagnostics,
42
53
  ...lintResult.diagnostics,
54
+ ...unreachableCodeDiagnostics,
43
55
  ]
44
56
  }
45
57
 
@@ -47,6 +59,12 @@ export class Diagnostics {
47
59
  this.sendDiagnosticsFor(textDocument)
48
60
  }
49
61
 
62
+ private getUnreachableCodeDiagnostics(document: Node): Diagnostic[] {
63
+ const collector = new UnreachableCodeCollector()
64
+ collector.visit(document)
65
+ return collector.diagnostics
66
+ }
67
+
50
68
  async refreshDocument(document: TextDocument) {
51
69
  await this.validate(document)
52
70
  }
@@ -67,3 +85,48 @@ export class Diagnostics {
67
85
  this.diagnostics.delete(textDocument)
68
86
  }
69
87
  }
88
+
89
+ export class UnreachableCodeCollector extends Visitor {
90
+ diagnostics: Diagnostic[] = []
91
+
92
+ visitERBCaseNode(node: ERBCaseNode): void {
93
+ this.checkUnreachableChildren(node.children)
94
+ this.visitChildNodes(node)
95
+ }
96
+
97
+ visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
98
+ this.checkUnreachableChildren(node.children)
99
+ this.visitChildNodes(node)
100
+ }
101
+
102
+ private checkUnreachableChildren(children: Node[]): void {
103
+ for (const child of children) {
104
+ if (isHTMLTextNode(child) && child.content.trim() === "") {
105
+ continue
106
+ }
107
+
108
+ const diagnostic: Diagnostic = {
109
+ range: {
110
+ start: {
111
+ line: this.toZeroBased(child.location.start.line),
112
+ character: child.location.start.column
113
+ },
114
+ end: {
115
+ line: this.toZeroBased(child.location.end.line),
116
+ character: child.location.end.column
117
+ }
118
+ },
119
+ message: "Unreachable code: content between case and when/in is never executed",
120
+ severity: DiagnosticSeverity.Hint,
121
+ tags: [DiagnosticTag.Unnecessary],
122
+ source: "Herb Language Server"
123
+ }
124
+
125
+ this.diagnostics.push(diagnostic)
126
+ }
127
+ }
128
+
129
+ private toZeroBased(line: number): number {
130
+ return line - 1
131
+ }
132
+ }
@@ -18,12 +18,31 @@ export class DocumentSaveService {
18
18
  this.formattingService = formattingService
19
19
  }
20
20
 
21
+ /**
22
+ * Apply only autofix edits on save.
23
+ * Called by willSaveWaitUntil - formatting is handled separately by editor.formatOnSave
24
+ */
25
+ async applyFixes(document: TextDocument): Promise<TextEdit[]> {
26
+ const settings = await this.settings.getDocumentSettings(document.uri)
27
+ const fixOnSave = settings?.linter?.fixOnSave !== false
28
+
29
+ this.connection.console.log(`[DocumentSave] applyFixes fixOnSave=${fixOnSave}`)
30
+
31
+ if (!fixOnSave) return []
32
+
33
+ return this.autofixService.autofix(document)
34
+ }
35
+
36
+ /**
37
+ * Apply autofix and formatting.
38
+ * Called by onDocumentFormatting (manual format or editor.formatOnSave)
39
+ */
21
40
  async applyFixesAndFormatting(document: TextDocument, reason: TextDocumentSaveReason): Promise<TextEdit[]> {
22
41
  const settings = await this.settings.getDocumentSettings(document.uri)
23
42
  const fixOnSave = settings?.linter?.fixOnSave !== false
24
43
  const formatterEnabled = settings?.formatter?.enabled ?? false
25
44
 
26
- this.connection.console.log(`[DocumentSave] fixOnSave=${fixOnSave}, formatterEnabled=${formatterEnabled}`)
45
+ this.connection.console.log(`[DocumentSave] applyFixesAndFormatting fixOnSave=${fixOnSave}, formatterEnabled=${formatterEnabled}`)
27
46
 
28
47
  let autofixEdits: TextEdit[] = []
29
48
 
@@ -210,6 +210,9 @@ export class FormattingService {
210
210
  if (filePath.endsWith('.herb.yml')) return false
211
211
  if (!this.config) return true
212
212
 
213
+ const hasConfigFile = Config.exists(this.config.projectPath)
214
+ if (!hasConfigFile) return true
215
+
213
216
  const relativePath = filePath.replace('file://', '').replace(this.project.projectPath + '/', '')
214
217
 
215
218
  return this.config.isFormatterEnabledForPath(relativePath)
package/src/server.ts CHANGED
@@ -12,15 +12,12 @@ import {
12
12
  DocumentRangeFormattingParams,
13
13
  CodeActionParams,
14
14
  CodeActionKind,
15
- TextEdit,
16
15
  } from "vscode-languageserver/node"
17
16
 
18
17
  import { Service } from "./service"
19
18
  import { PersonalHerbSettings } from "./settings"
20
19
  import { Config } from "@herb-tools/config"
21
20
 
22
- import type { TextDocument } from "vscode-languageserver-textdocument"
23
-
24
21
  export class Server {
25
22
  private service!: Service
26
23
  private connection: Connection
@@ -37,7 +34,7 @@ export class Server {
37
34
  await this.service.init()
38
35
 
39
36
  this.service.documentService.documents.onWillSaveWaitUntil(async (event) => {
40
- return this.service.documentSaveService.applyFixesAndFormatting(event.document, event.reason)
37
+ return this.service.documentSaveService.applyFixes(event.document)
41
38
  })
42
39
 
43
40
  const result: InitializeResult = {
@@ -89,7 +86,6 @@ export class Server {
89
86
  watchers: [
90
87
  ...patterns,
91
88
  { globPattern: `**/.herb.yml` },
92
- { globPattern: `**/**/.herb-lsp/config.json` },
93
89
  { globPattern: `**/.herb/rules/**/*.mjs` },
94
90
  { globPattern: `**/.herb/rewriters/**/*.mjs` },
95
91
  ],
@@ -119,7 +115,7 @@ export class Server {
119
115
 
120
116
  this.connection.onDidChangeWatchedFiles(async (params) => {
121
117
  for (const event of params.changes) {
122
- const isConfigChange = event.uri.endsWith("/.herb.yml") || event.uri.endsWith("/.herb-lsp/config.json")
118
+ const isConfigChange = event.uri.endsWith("/.herb.yml")
123
119
  const isCustomRuleChange = event.uri.includes("/.herb/rules/")
124
120
  const isCustomRewriterChange = event.uri.includes("/.herb/rewriters/")
125
121
 
package/src/settings.ts CHANGED
@@ -87,8 +87,9 @@ export class Settings {
87
87
  // TODO: ideally we can just use Config all the way through
88
88
  private mergeSettings(userSettings: PersonalHerbSettings | null, projectConfig?: Config): PersonalHerbSettings {
89
89
  const settings = userSettings || this.defaultSettings
90
+ const hasConfigFile = projectConfig ? Config.exists(projectConfig.projectPath) : false
90
91
 
91
- if (!projectConfig) {
92
+ if (!projectConfig || !hasConfigFile) {
92
93
  return {
93
94
  trace: settings.trace,
94
95
  linter: {
package/dist/config.js DELETED
@@ -1,73 +0,0 @@
1
- import path from "path";
2
- import { version } from "../package.json";
3
- import { promises as fs } from "fs";
4
- export class Config {
5
- constructor(projectPath, config) {
6
- this.path = Config.configPathFromProjectPath(projectPath);
7
- this.config = config;
8
- }
9
- get version() {
10
- return this.config.version;
11
- }
12
- get createdAt() {
13
- return new Date(this.config.createdAt);
14
- }
15
- get updatedAt() {
16
- return new Date(this.config.updatedAt);
17
- }
18
- get options() {
19
- return this.config.options;
20
- }
21
- toJSON() {
22
- return JSON.stringify(this.config, null, " ");
23
- }
24
- updateTimestamp() {
25
- this.config.updatedAt = new Date().toISOString();
26
- }
27
- updateVersion() {
28
- this.config.version = version;
29
- }
30
- async write() {
31
- this.updateVersion();
32
- this.updateTimestamp();
33
- const folder = path.dirname(this.path);
34
- fs.stat(folder)
35
- .then(() => { })
36
- .catch(async () => await fs.mkdir(folder))
37
- .finally(async () => await fs.writeFile(this.path, this.toJSON()));
38
- }
39
- async read() {
40
- return await fs.readFile(this.path, "utf8");
41
- }
42
- static configPathFromProjectPath(projectPath) {
43
- return path.join(projectPath, this.configPath);
44
- }
45
- static async fromPathOrNew(projectPath) {
46
- try {
47
- return await this.fromPath(projectPath);
48
- }
49
- catch {
50
- return Config.newConfig(projectPath);
51
- }
52
- }
53
- static async fromPath(projectPath) {
54
- const configPath = Config.configPathFromProjectPath(projectPath);
55
- try {
56
- const config = JSON.parse(await fs.readFile(configPath, "utf8"));
57
- return new Config(projectPath, config);
58
- }
59
- catch (error) {
60
- throw new Error(`Error reading config file at: ${configPath}. Error: ${error.message}`);
61
- }
62
- }
63
- static newConfig(projectPath) {
64
- return new Config(projectPath, {
65
- version,
66
- createdAt: new Date().toISOString(),
67
- updatedAt: new Date().toISOString(),
68
- options: {}
69
- });
70
- }
71
- }
72
- Config.configPath = ".herb-lsp/config.json";
73
- //# sourceMappingURL=config.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -1,34 +0,0 @@
1
- export type HerbConfigOptions = {
2
- formatter?: {
3
- enabled?: boolean;
4
- include?: string[];
5
- exclude?: string[];
6
- indentWidth?: number;
7
- maxLineLength?: number;
8
- };
9
- };
10
- export type HerbLSPConfig = {
11
- version: string;
12
- createdAt: string;
13
- updatedAt: string;
14
- options: HerbConfigOptions;
15
- };
16
- export declare class Config {
17
- static configPath: string;
18
- readonly path: string;
19
- config: HerbLSPConfig;
20
- constructor(projectPath: string, config: HerbLSPConfig);
21
- get version(): string;
22
- get createdAt(): Date;
23
- get updatedAt(): Date;
24
- get options(): HerbConfigOptions;
25
- toJSON(): string;
26
- private updateTimestamp;
27
- private updateVersion;
28
- write(): Promise<void>;
29
- read(): Promise<string>;
30
- static configPathFromProjectPath(projectPath: string): string;
31
- static fromPathOrNew(projectPath: string): Promise<Config>;
32
- static fromPath(projectPath: string): Promise<Config>;
33
- static newConfig(projectPath: string): Config;
34
- }
package/src/config.ts DELETED
@@ -1,109 +0,0 @@
1
- export type HerbConfigOptions = {
2
- formatter?: {
3
- enabled?: boolean
4
- include?: string[]
5
- exclude?: string[]
6
- indentWidth?: number
7
- maxLineLength?: number
8
- }
9
- }
10
-
11
- export type HerbLSPConfig = {
12
- version: string
13
- createdAt: string
14
- updatedAt: string
15
- options: HerbConfigOptions
16
- }
17
-
18
- import path from "path"
19
- import { version } from "../package.json"
20
- import { promises as fs } from "fs"
21
-
22
- export class Config {
23
- static configPath = ".herb-lsp/config.json"
24
-
25
- public readonly path: string
26
- public config: HerbLSPConfig
27
-
28
- constructor(projectPath: string, config: HerbLSPConfig) {
29
- this.path = Config.configPathFromProjectPath(projectPath)
30
- this.config = config
31
- }
32
-
33
- get version(): string {
34
- return this.config.version
35
- }
36
-
37
- get createdAt(): Date {
38
- return new Date(this.config.createdAt)
39
- }
40
-
41
- get updatedAt(): Date {
42
- return new Date(this.config.updatedAt)
43
- }
44
-
45
- get options(): HerbConfigOptions {
46
- return this.config.options
47
- }
48
-
49
- public toJSON() {
50
- return JSON.stringify(this.config, null, " ")
51
- }
52
-
53
- private updateTimestamp() {
54
- this.config.updatedAt = new Date().toISOString()
55
- }
56
-
57
- private updateVersion() {
58
- this.config.version = version
59
- }
60
-
61
- async write() {
62
- this.updateVersion()
63
- this.updateTimestamp()
64
-
65
- const folder = path.dirname(this.path)
66
-
67
- fs.stat(folder)
68
- .then(() => {})
69
- .catch(async () => await fs.mkdir(folder))
70
- .finally(async () => await fs.writeFile(this.path, this.toJSON()))
71
- }
72
-
73
- async read() {
74
- return await fs.readFile(this.path, "utf8")
75
- }
76
-
77
- static configPathFromProjectPath(projectPath: string) {
78
- return path.join(projectPath, this.configPath)
79
- }
80
-
81
- static async fromPathOrNew(projectPath: string) {
82
- try {
83
- return await this.fromPath(projectPath)
84
- } catch {
85
- return Config.newConfig(projectPath)
86
- }
87
- }
88
-
89
- static async fromPath(projectPath: string) {
90
- const configPath = Config.configPathFromProjectPath(projectPath)
91
-
92
- try {
93
- const config = JSON.parse(await fs.readFile(configPath, "utf8"))
94
-
95
- return new Config(projectPath, config)
96
- } catch (error: any) {
97
- throw new Error(`Error reading config file at: ${configPath}. Error: ${error.message}`)
98
- }
99
- }
100
-
101
- static newConfig(projectPath: string): Config {
102
- return new Config(projectPath, {
103
- version,
104
- createdAt: new Date().toISOString(),
105
- updatedAt: new Date().toISOString(),
106
- options: {}
107
- })
108
- }
109
- }