@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.
- package/SailsParser.js +55 -17
- package/completions/model-attribute-props-completion.js +20 -19
- package/completions/model-attributes-completion.js +350 -0
- package/completions/model-methods-completion.js +24 -1
- package/go-to-definitions/go-to-action.js +82 -29
- package/go-to-definitions/go-to-page.js +40 -26
- package/go-to-definitions/go-to-view.js +42 -24
- package/index.js +1 -1
- package/package.json +1 -1
- package/validators/validate-action-exist.js +54 -15
- package/validators/validate-data-type.js +88 -25
- package/validators/validate-helper-input-exist.js +98 -32
- package/validators/validate-model-attribute-exist.js +162 -55
- package/validators/validate-model-exist.js +14 -0
- package/validators/validate-required-helper-input.js +99 -39
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
try {
|
|
12
|
+
const ast = acorn.parse(text, {
|
|
13
|
+
ecmaVersion: 'latest',
|
|
14
|
+
sourceType: 'module'
|
|
15
|
+
})
|
|
12
16
|
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
+
try {
|
|
18
|
+
const ast = acorn.parse(text, {
|
|
19
|
+
ecmaVersion: 'latest',
|
|
20
|
+
sourceType: 'module'
|
|
21
|
+
})
|
|
17
22
|
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
package/package.json
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|