@sailshq/language-server 0.3.2 → 0.4.0

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,38 +1,52 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
4
+
2
5
  module.exports = async function goToPage(document, position, typeMap) {
3
6
  const filePath = document.uri
4
7
  if (!filePath.includes('/api/controllers/')) return null
5
8
  const text = document.getText()
6
9
  const offset = document.offsetAt(position)
7
10
 
8
- const regex =
9
- /{[^}]*?\bpage\s*:\s*(?<quote>['"])(?<page>[^'"]+)\k<quote>[^}]*?}/g
10
-
11
- let match
11
+ try {
12
+ const ast = acorn.parse(text, {
13
+ ecmaVersion: 'latest',
14
+ sourceType: 'module'
15
+ })
12
16
 
13
- while ((match = regex.exec(text)) !== null) {
14
- const pageName = match.groups.page
15
- const quote = match.groups.quote
16
- const fullMatchStart =
17
- match.index + match[0].indexOf(quote + pageName + quote)
18
- const fullMatchEnd = fullMatchStart + pageName.length + 2 // +2 for quotes
17
+ let result = null
19
18
 
20
- if (offset >= fullMatchStart && offset <= fullMatchEnd) {
21
- const pagePath = typeMap.pages?.[pageName]
22
- if (pagePath) {
23
- const uri = `file://${pagePath.path}`
24
- return lsp.LocationLink.create(
25
- uri,
26
- lsp.Range.create(0, 0, 0, 0),
27
- lsp.Range.create(0, 0, 0, 0),
28
- lsp.Range.create(
29
- document.positionAt(fullMatchStart),
30
- document.positionAt(fullMatchEnd)
31
- )
32
- )
19
+ walk.simple(ast, {
20
+ Property(node) {
21
+ if (
22
+ node.key &&
23
+ (node.key.name === 'page' || node.key.value === 'page') &&
24
+ node.value &&
25
+ node.value.type === 'Literal' &&
26
+ typeof node.value.value === 'string'
27
+ ) {
28
+ const pageName = node.value.value
29
+ if (offset >= node.value.start && offset <= node.value.end) {
30
+ const pagePath = typeMap.pages?.[pageName]
31
+ if (pagePath) {
32
+ const uri = `file://${pagePath.path}`
33
+ result = lsp.LocationLink.create(
34
+ uri,
35
+ lsp.Range.create(0, 0, 0, 0),
36
+ lsp.Range.create(0, 0, 0, 0),
37
+ lsp.Range.create(
38
+ document.positionAt(node.value.start),
39
+ document.positionAt(node.value.end)
40
+ )
41
+ )
42
+ }
43
+ }
44
+ }
33
45
  }
34
- }
35
- }
46
+ })
36
47
 
37
- return null
48
+ return result
49
+ } catch (error) {
50
+ return null
51
+ }
38
52
  }
@@ -1,5 +1,7 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const path = require('path')
3
+ const acorn = require('acorn')
4
+ const walk = require('acorn-walk')
3
5
 
4
6
  module.exports = async function goToView(document, position, typeMap) {
5
7
  const fileName = path.basename(document.uri)
@@ -12,32 +14,48 @@ module.exports = async function goToView(document, position, typeMap) {
12
14
 
13
15
  if (!isRoutes && !isController) return null
14
16
 
15
- const regex =
16
- /\b(viewTemplatePath|view)\s*:\s*(?<quote>['"])(?<view>[^'"]+)\k<quote>/g
17
+ try {
18
+ const ast = acorn.parse(text, {
19
+ ecmaVersion: 'latest',
20
+ sourceType: 'module'
21
+ })
17
22
 
18
- let match
19
- while ((match = regex.exec(text)) !== null) {
20
- const viewName = match.groups.view
21
- const quote = match.groups.quote
22
- const fullMatchStart =
23
- match.index + match[0].indexOf(quote + viewName + quote)
24
- const fullMatchEnd = fullMatchStart + viewName.length + 2
23
+ let result = null
25
24
 
26
- if (offset >= fullMatchStart && offset <= fullMatchEnd) {
27
- const viewPath = typeMap.views?.[viewName]
28
- if (viewPath) {
29
- const uri = `file://${viewPath.path}`
30
- return lsp.LocationLink.create(
31
- uri,
32
- lsp.Range.create(0, 0, 0, 0),
33
- lsp.Range.create(0, 0, 0, 0),
34
- lsp.Range.create(
35
- document.positionAt(fullMatchStart),
36
- document.positionAt(fullMatchEnd)
37
- )
38
- )
25
+ walk.simple(ast, {
26
+ Property(node) {
27
+ if (
28
+ node.key &&
29
+ (node.key.name === 'viewTemplatePath' ||
30
+ node.key.value === 'viewTemplatePath' ||
31
+ node.key.name === 'view' ||
32
+ node.key.value === 'view') &&
33
+ node.value &&
34
+ node.value.type === 'Literal' &&
35
+ typeof node.value.value === 'string'
36
+ ) {
37
+ const viewName = node.value.value
38
+ if (offset >= node.value.start && offset <= node.value.end) {
39
+ const viewPath = typeMap.views?.[viewName]
40
+ if (viewPath) {
41
+ const uri = `file://${viewPath.path}`
42
+ result = lsp.LocationLink.create(
43
+ uri,
44
+ lsp.Range.create(0, 0, 0, 0),
45
+ lsp.Range.create(0, 0, 0, 0),
46
+ lsp.Range.create(
47
+ document.positionAt(node.value.start),
48
+ document.positionAt(node.value.end)
49
+ )
50
+ )
51
+ }
52
+ }
53
+ }
39
54
  }
40
- }
55
+ })
56
+
57
+ return result
58
+ } catch (error) {
59
+ return null
41
60
  }
42
- return null
43
61
  }
package/index.js CHANGED
@@ -100,7 +100,7 @@ connection.onDefinition(async (params) => {
100
100
  policyDefinition,
101
101
  helperDefinition,
102
102
  modelDefinition
103
- ].filter(Boolean)
103
+ ].filter((def) => def && (Array.isArray(def) ? def.length > 0 : true))
104
104
  return definitions
105
105
  })
106
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -1,4 +1,6 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
2
4
 
3
5
  module.exports = function validateActionExist(document, typeMap) {
4
6
  const diagnostics = []
@@ -28,23 +30,60 @@ module.exports = function validateActionExist(document, typeMap) {
28
30
 
29
31
  function extractActionInfo(document) {
30
32
  const text = document.getText()
31
- const regex = /(['"])(.+?)\1\s*:\s*(?:{?\s*action\s*:\s*)?(['"])(.+?)\3/g
32
33
  const actions = []
33
- let match
34
-
35
- while ((match = regex.exec(text)) !== null) {
36
- const action = match[4]
37
- const actionStart = match.index + match[0].lastIndexOf(action)
38
- const actionEnd = actionStart + action.length
39
-
40
- actions.push({
41
- action,
42
- range: lsp.Range.create(
43
- document.positionAt(actionStart),
44
- document.positionAt(actionEnd)
45
- )
34
+
35
+ try {
36
+ const ast = acorn.parse(text, {
37
+ ecmaVersion: 'latest',
38
+ sourceType: 'module'
46
39
  })
47
- }
40
+
41
+ walk.simple(ast, {
42
+ Property(node) {
43
+ const propertyKey = node.key?.value || node.key?.name
44
+
45
+ if (
46
+ propertyKey === 'action' &&
47
+ node.value?.type === 'Literal' &&
48
+ typeof node.value.value === 'string'
49
+ ) {
50
+ const actionName = node.value.value
51
+ actions.push({
52
+ action: actionName,
53
+ range: lsp.Range.create(
54
+ document.positionAt(node.value.start),
55
+ document.positionAt(node.value.end)
56
+ )
57
+ })
58
+ } else if (
59
+ typeof propertyKey === 'string' &&
60
+ (propertyKey.includes('GET') ||
61
+ propertyKey.includes('POST') ||
62
+ propertyKey.includes('PUT') ||
63
+ propertyKey.includes('PATCH') ||
64
+ propertyKey.includes('DELETE') ||
65
+ propertyKey.includes('/')) &&
66
+ node.value?.type === 'Literal' &&
67
+ typeof node.value.value === 'string'
68
+ ) {
69
+ const actionName = node.value.value
70
+ if (
71
+ !actionName.startsWith('/') &&
72
+ !actionName.startsWith('http://') &&
73
+ !actionName.startsWith('https://')
74
+ ) {
75
+ actions.push({
76
+ action: actionName,
77
+ range: lsp.Range.create(
78
+ document.positionAt(node.value.start),
79
+ document.positionAt(node.value.end)
80
+ )
81
+ })
82
+ }
83
+ }
84
+ }
85
+ })
86
+ } catch (error) {}
48
87
 
49
88
  return actions
50
89
  }
@@ -1,34 +1,97 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
2
4
 
3
5
  module.exports = function validateDataType(document, typeMap) {
4
6
  const diagnostics = []
5
-
6
7
  const text = document.getText()
7
8
 
8
- // Regex to match lines like: type: 'string' or type: "number"
9
- const regex = /type\s*:\s*['"]([a-zA-Z0-9_-]+)['"]/g
10
-
11
- let match
12
- while ((match = regex.exec(text)) !== null) {
13
- const dataType = match[1]
14
- const typeStart = match.index + match[0].indexOf(dataType)
15
- const typeEnd = typeStart + dataType.length
16
-
17
- const isValid = typeMap.dataTypes.some((dt) => dt.type === dataType)
18
-
19
- if (!isValid) {
20
- diagnostics.push(
21
- lsp.Diagnostic.create(
22
- lsp.Range.create(
23
- document.positionAt(typeStart),
24
- document.positionAt(typeEnd)
25
- ),
26
- `'${dataType}' is not a recognized data type. Valid data types are: ${typeMap.dataTypes.map((dataType) => dataType.type).join(', ')}.`,
27
- lsp.DiagnosticSeverity.Error,
28
- 'sails-lsp'
29
- )
30
- )
31
- }
9
+ try {
10
+ const ast = acorn.parse(text, {
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module'
13
+ })
14
+
15
+ walk.simple(ast, {
16
+ ObjectExpression(node) {
17
+ // Check if this object has both 'type' and other properties that suggest it's a definition
18
+ // (like 'required', 'description', 'allowNull', 'defaultsTo', 'example', etc.)
19
+ let hasTypeProperty = false
20
+ let typePropertyNode = null
21
+ let typeValue = null
22
+ let hasDefinitionProperties = false
23
+
24
+ for (const prop of node.properties) {
25
+ if (prop.type !== 'Property') continue
26
+
27
+ const keyName = prop.key.name || prop.key.value
28
+
29
+ // Check if this is a 'type' property
30
+ if (keyName === 'type' && prop.value.type === 'Literal') {
31
+ hasTypeProperty = true
32
+ typePropertyNode = prop.value
33
+ typeValue = prop.value.value
34
+ }
35
+
36
+ // Check for properties that indicate this is a model/action/helper definition
37
+ if (
38
+ [
39
+ 'required',
40
+ 'description',
41
+ 'allowNull',
42
+ 'defaultsTo',
43
+ 'columnName',
44
+ 'columnType',
45
+ 'autoMigrations',
46
+ 'autoCreatedAt',
47
+ 'autoUpdatedAt',
48
+ 'model',
49
+ 'collection',
50
+ 'via',
51
+ 'through',
52
+ 'unique',
53
+ 'isEmail',
54
+ 'isURL',
55
+ 'isIn',
56
+ 'min',
57
+ 'max',
58
+ 'minLength',
59
+ 'maxLength',
60
+ 'example',
61
+ 'validations',
62
+ 'regex',
63
+ 'extendedDescription',
64
+ 'moreInfoUrl',
65
+ 'whereToGet'
66
+ ].includes(keyName)
67
+ ) {
68
+ hasDefinitionProperties = true
69
+ }
70
+ }
71
+
72
+ // Only validate if this looks like an attribute/input definition
73
+ if (hasTypeProperty && hasDefinitionProperties && typeValue) {
74
+ const isValid = typeMap.dataTypes.some((dt) => dt.type === typeValue)
75
+
76
+ if (!isValid) {
77
+ diagnostics.push(
78
+ lsp.Diagnostic.create(
79
+ lsp.Range.create(
80
+ document.positionAt(typePropertyNode.start),
81
+ document.positionAt(typePropertyNode.end)
82
+ ),
83
+ `'${typeValue}' is not a recognized data type. Valid data types are: ${typeMap.dataTypes.map((dataType) => dataType.type).join(', ')}.`,
84
+ lsp.DiagnosticSeverity.Error,
85
+ 'sails-lsp'
86
+ )
87
+ )
88
+ }
89
+ }
90
+ }
91
+ })
92
+ } catch (error) {
93
+ // Ignore parse errors
32
94
  }
95
+
33
96
  return diagnostics
34
97
  }
@@ -1,42 +1,108 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
2
4
 
3
5
  module.exports = function validateHelperInputExist(document, typeMap) {
4
6
  const diagnostics = []
5
7
  const text = document.getText()
6
8
 
7
- // Regex to match sails.helpers.foo.bar.with({ ... })
8
- // Captures: 1) helper path, 2) object literal content
9
- const regex = /sails\.helpers((?:\.[a-zA-Z0-9_]+)+)\.with\s*\(\s*\{([^}]*)\}/g
10
- let match
11
- while ((match = regex.exec(text)) !== null) {
12
- // Build helper name: e.g. .foo.bar => foo/bar
13
- const segments = match[1].split('.').filter(Boolean)
14
- const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
15
- const fullHelperName = segments.map(toKebab).join('/')
16
- const helperInfo = typeMap.helpers && typeMap.helpers[fullHelperName]
17
- if (!helperInfo || !helperInfo.inputs) continue
18
-
19
- // Find all property names in the object literal
20
- const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
21
- let propMatch
22
- while ((propMatch = propsRegex.exec(match[2])) !== null) {
23
- const key = propMatch[1]
24
- if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
25
- const start = match.index + match[0].indexOf(key)
26
- const end = start + key.length
27
- diagnostics.push(
28
- lsp.Diagnostic.create(
29
- lsp.Range.create(
30
- document.positionAt(start),
31
- document.positionAt(end)
32
- ),
33
- `Unknown input property '${key}' for helper '${fullHelperName}'.`,
34
- lsp.DiagnosticSeverity.Error,
35
- 'sails-lsp'
36
- )
37
- )
9
+ try {
10
+ const ast = acorn.parse(text, {
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module'
13
+ })
14
+
15
+ walk.simple(ast, {
16
+ CallExpression(node) {
17
+ // Match sails.helpers.foo.bar.with({ ... })
18
+ if (
19
+ node.callee &&
20
+ node.callee.type === 'MemberExpression' &&
21
+ node.callee.property.name === 'with' &&
22
+ node.callee.object &&
23
+ node.callee.object.type === 'MemberExpression'
24
+ ) {
25
+ // Extract helper path from sails.helpers.foo.bar
26
+ const helperPath = extractHelperPath(node.callee.object)
27
+ if (!helperPath) return
28
+
29
+ const helperInfo = typeMap.helpers && typeMap.helpers[helperPath]
30
+ if (!helperInfo || !helperInfo.inputs) return
31
+
32
+ // Get the object argument to .with()
33
+ const objArg = node.arguments[0]
34
+ if (!objArg || objArg.type !== 'ObjectExpression') return
35
+
36
+ // Validate each property in the object
37
+ for (const prop of objArg.properties) {
38
+ // Handle both regular properties and shorthand properties
39
+ let key
40
+ if (prop.type === 'Property') {
41
+ if (prop.key.type === 'Identifier') {
42
+ key = prop.key.name
43
+ } else if (prop.key.type === 'Literal') {
44
+ key = prop.key.value
45
+ }
46
+ } else if (prop.type === 'SpreadElement') {
47
+ // Skip spread elements
48
+ continue
49
+ }
50
+
51
+ if (!key) continue
52
+
53
+ // Check if this key exists in the helper's inputs
54
+ if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
55
+ diagnostics.push(
56
+ lsp.Diagnostic.create(
57
+ lsp.Range.create(
58
+ document.positionAt(prop.key.start),
59
+ document.positionAt(prop.key.end)
60
+ ),
61
+ `Unknown input property '${key}' for helper '${helperPath}'.`,
62
+ lsp.DiagnosticSeverity.Error,
63
+ 'sails-lsp'
64
+ )
65
+ )
66
+ }
67
+ }
68
+ }
38
69
  }
39
- }
70
+ })
71
+ } catch (error) {
72
+ // Ignore parse errors
40
73
  }
74
+
41
75
  return diagnostics
42
76
  }
77
+
78
+ function extractHelperPath(node) {
79
+ // Walk up the member expression to extract the full helper path
80
+ const segments = []
81
+ let current = node
82
+
83
+ // Collect all segments until we reach sails.helpers
84
+ while (current && current.type === 'MemberExpression') {
85
+ if (current.property && current.property.type === 'Identifier') {
86
+ const propName = current.property.name
87
+ // Stop when we reach 'helpers'
88
+ if (propName === 'helpers') {
89
+ // Check if the object is 'sails'
90
+ if (
91
+ current.object &&
92
+ current.object.type === 'Identifier' &&
93
+ current.object.name === 'sails'
94
+ ) {
95
+ // Valid sails.helpers path found
96
+ const toKebab = (s) =>
97
+ s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
98
+ return segments.map(toKebab).join('/')
99
+ }
100
+ return null
101
+ }
102
+ segments.unshift(propName)
103
+ }
104
+ current = current.object
105
+ }
106
+
107
+ return null
108
+ }