aws-iam-language-server 0.0.10 → 0.0.11
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/package.json +1 -1
- package/readme.md +11 -0
- package/src/handlers/diagnostics/action.d.ts +6 -0
- package/src/handlers/diagnostics/action.js +14 -0
- package/src/handlers/diagnostics/base.d.ts +9 -0
- package/src/handlers/diagnostics/base.js +21 -0
- package/src/handlers/diagnostics/condition.d.ts +6 -0
- package/src/handlers/diagnostics/condition.js +7 -0
- package/src/handlers/diagnostics/diagnostics.d.ts +4 -0
- package/src/handlers/diagnostics/diagnostics.js +121 -0
- package/src/handlers/diagnostics/effect.d.ts +6 -0
- package/src/handlers/diagnostics/effect.js +12 -0
- package/src/handlers/diagnostics/principal.d.ts +6 -0
- package/src/handlers/diagnostics/principal.js +7 -0
- package/src/handlers/diagnostics/resource.d.ts +6 -0
- package/src/handlers/diagnostics/resource.js +7 -0
- package/src/handlers/diagnostics/sid.d.ts +8 -0
- package/src/handlers/diagnostics/sid.js +21 -0
- package/src/handlers/diagnostics/utils.d.ts +3 -0
- package/src/handlers/diagnostics/utils.js +19 -0
- package/src/handlers/document-link/document-link.d.ts +1 -1
- package/src/handlers/document-link/document-link.js +37 -27
- package/src/lib/iam-policy/reference/services.d.ts +1 -1
- package/src/lib/iam-policy/reference/services.js +4 -2
- package/src/lib/treesitter/base.d.ts +28 -2
- package/src/lib/treesitter/base.js +9 -0
- package/src/lib/treesitter/hcl.d.ts +2 -1
- package/src/lib/treesitter/hcl.js +262 -1
- package/src/lib/treesitter/json.d.ts +2 -1
- package/src/lib/treesitter/json.js +126 -1
- package/src/lib/treesitter/yaml.d.ts +2 -1
- package/src/lib/treesitter/yaml.js +135 -1
- package/src/server.js +12 -3
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -58,3 +58,14 @@ This language server provides completion on:
|
|
|
58
58
|
- resources (progressive arn component suggestions, full arn completions for action-specific arns)
|
|
59
59
|
- condition operators (`StringLike`, `ForAnyValue:*`, etc)
|
|
60
60
|
- condition keys (global keys like `aws:RequestTag/${TagKey}`, action-specific keys like `s3:TlsVersion`)
|
|
61
|
+
|
|
62
|
+
### Diagnostics
|
|
63
|
+
|
|
64
|
+
This language server will provide diagnostics for some IAM policy issues, including:
|
|
65
|
+
|
|
66
|
+
- no extra policy document keys are specified
|
|
67
|
+
- no missing keys in a statement, (effect, action, resource or effect, action, principal)
|
|
68
|
+
- no duplicate keys in a statement (including "not" variants like action/not action)
|
|
69
|
+
- ensuring `Sid` uniqueness within a policy document
|
|
70
|
+
- effect has a valid value
|
|
71
|
+
- defined actions are valid, or wildcards resolve to valid actions
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
import { ElementValidator } from './base.ts';
|
|
4
|
+
export declare class ActionValidator extends ElementValidator {
|
|
5
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expandActionPattern } from "../../lib/iam-policy/wildcard.js";
|
|
2
|
+
import { ElementValidator } from "./base.js";
|
|
3
|
+
import { createDiagnostic } from "./utils.js";
|
|
4
|
+
export class ActionValidator extends ElementValidator {
|
|
5
|
+
validate(entry) {
|
|
6
|
+
const diagnostics = super.validate(entry);
|
|
7
|
+
for (const value of entry.values) {
|
|
8
|
+
if (expandActionPattern(value.text).length === 0) {
|
|
9
|
+
diagnostics.push(createDiagnostic(`Unrecognized action "${value.text}"`, value.range));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return diagnostics;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
export declare class ElementValidator {
|
|
4
|
+
#private;
|
|
5
|
+
constructor();
|
|
6
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
7
|
+
isValidated(): boolean;
|
|
8
|
+
resetForStatement(): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createDiagnostic } from "./utils.js";
|
|
2
|
+
export class ElementValidator {
|
|
3
|
+
#validated;
|
|
4
|
+
constructor() {
|
|
5
|
+
this.#validated = false;
|
|
6
|
+
}
|
|
7
|
+
validate(entry) {
|
|
8
|
+
const diagnostics = [];
|
|
9
|
+
if (this.#validated) {
|
|
10
|
+
diagnostics.push(createDiagnostic('duplicate statement key', entry.keyRange));
|
|
11
|
+
}
|
|
12
|
+
this.#validated = true;
|
|
13
|
+
return diagnostics;
|
|
14
|
+
}
|
|
15
|
+
isValidated() {
|
|
16
|
+
return this.#validated;
|
|
17
|
+
}
|
|
18
|
+
resetForStatement() {
|
|
19
|
+
this.#validated = false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
import { ElementValidator } from './base.ts';
|
|
4
|
+
export declare class ConditionValidator extends ElementValidator {
|
|
5
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Connection } from 'vscode-languageserver';
|
|
2
|
+
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
3
|
+
import type { TreeManager } from '../../lib/treesitter/manager.ts';
|
|
4
|
+
export declare function diagnosticsHandler(document: TextDocument, treeManager: TreeManager, connection: Connection): Promise<void>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ActionValidator } from "./action.js";
|
|
2
|
+
import { ConditionValidator } from "./condition.js";
|
|
3
|
+
import { EffectValidator } from "./effect.js";
|
|
4
|
+
import { PrincipalValidator } from "./principal.js";
|
|
5
|
+
import { ResourceValidator } from "./resource.js";
|
|
6
|
+
import { SidValidator } from "./sid.js";
|
|
7
|
+
import { createDiagnostic } from "./utils.js";
|
|
8
|
+
export async function diagnosticsHandler(document, treeManager, connection) {
|
|
9
|
+
const handler = treeManager.getLanguageHandler(document.uri);
|
|
10
|
+
if (!handler)
|
|
11
|
+
return;
|
|
12
|
+
let diagnostics = [];
|
|
13
|
+
const policyDocuments = handler.getAllPolicyDocuments(document.uri);
|
|
14
|
+
for (const policyDocument of policyDocuments) {
|
|
15
|
+
if (policyDocument.policyFormat === 'standard') {
|
|
16
|
+
diagnostics = diagnostics.concat(await handleStandardDiagnostics(policyDocument));
|
|
17
|
+
}
|
|
18
|
+
else if (policyDocument.policyFormat === 'hcl-block') {
|
|
19
|
+
diagnostics = diagnostics.concat(await handleHclBlockDiagnostics(policyDocument));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
connection.console.log(`Publishing diagnostics for uri: ${document.uri}: ${policyDocuments.length} policy documents, ${diagnostics.length} diagnostics`);
|
|
23
|
+
await connection.sendDiagnostics({
|
|
24
|
+
uri: document.uri,
|
|
25
|
+
diagnostics,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function handleStandardDiagnostics(policyDocument) {
|
|
29
|
+
let diagnostics = [];
|
|
30
|
+
const sidValidator = new SidValidator();
|
|
31
|
+
const effectValidator = new EffectValidator();
|
|
32
|
+
const principalValidator = new PrincipalValidator();
|
|
33
|
+
const actionValidator = new ActionValidator();
|
|
34
|
+
const resourceValidator = new ResourceValidator();
|
|
35
|
+
const conditionValidator = new ConditionValidator();
|
|
36
|
+
for (const statement of policyDocument.statements) {
|
|
37
|
+
for (const entry of statement.entries) {
|
|
38
|
+
if (entry.key === 'Sid') {
|
|
39
|
+
diagnostics = diagnostics.concat(sidValidator.validate(entry));
|
|
40
|
+
}
|
|
41
|
+
else if (entry.key === 'Effect') {
|
|
42
|
+
diagnostics = diagnostics.concat(effectValidator.validate(entry));
|
|
43
|
+
}
|
|
44
|
+
else if (entry.key === 'Principal' || entry.key === 'NotPrincipal') {
|
|
45
|
+
diagnostics = diagnostics.concat(principalValidator.validate(entry));
|
|
46
|
+
}
|
|
47
|
+
else if (entry.key === 'Action' || entry.key === 'NotAction') {
|
|
48
|
+
diagnostics = diagnostics.concat(actionValidator.validate(entry));
|
|
49
|
+
}
|
|
50
|
+
else if (entry.key === 'Resource' || entry.key === 'NotResource') {
|
|
51
|
+
diagnostics = diagnostics.concat(resourceValidator.validate(entry));
|
|
52
|
+
}
|
|
53
|
+
else if (entry.key === 'Condition') {
|
|
54
|
+
diagnostics = diagnostics.concat(conditionValidator.validate(entry));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
diagnostics.push(createDiagnostic(`Unrecognized entry "${entry.key}" in statement`, entry.keyRange));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!effectValidator.isValidated()) {
|
|
61
|
+
diagnostics.push(createDiagnostic(`Missing required "Effect" entry in statement`, statement.range));
|
|
62
|
+
}
|
|
63
|
+
if (!actionValidator.isValidated()) {
|
|
64
|
+
diagnostics.push(createDiagnostic(`Missing required "Action" or "NotAction" entry in statement`, statement.range));
|
|
65
|
+
}
|
|
66
|
+
if (!resourceValidator.isValidated() && !principalValidator.isValidated()) {
|
|
67
|
+
diagnostics.push(createDiagnostic(`Missing required "Resource"/"NotResource" or "Principal"/"NotPrincipal" entry in statement`, statement.range));
|
|
68
|
+
}
|
|
69
|
+
[sidValidator, effectValidator, principalValidator, actionValidator, resourceValidator, conditionValidator].forEach((x) => {
|
|
70
|
+
x.resetForStatement();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return diagnostics;
|
|
74
|
+
}
|
|
75
|
+
async function handleHclBlockDiagnostics(policyDocument) {
|
|
76
|
+
let diagnostics = [];
|
|
77
|
+
const sidValidator = new SidValidator();
|
|
78
|
+
const effectValidator = new EffectValidator();
|
|
79
|
+
const principalValidator = new PrincipalValidator();
|
|
80
|
+
const actionValidator = new ActionValidator();
|
|
81
|
+
const resourceValidator = new ResourceValidator();
|
|
82
|
+
const conditionValidator = new ConditionValidator();
|
|
83
|
+
for (const statement of policyDocument.statements) {
|
|
84
|
+
for (const entry of statement.entries) {
|
|
85
|
+
if (entry.key === 'sid') {
|
|
86
|
+
diagnostics = diagnostics.concat(sidValidator.validate(entry));
|
|
87
|
+
}
|
|
88
|
+
else if (entry.key === 'effect') {
|
|
89
|
+
diagnostics = diagnostics.concat(effectValidator.validate(entry));
|
|
90
|
+
}
|
|
91
|
+
else if (entry.key === 'principals' || entry.key === 'not_principals') {
|
|
92
|
+
diagnostics = diagnostics.concat(principalValidator.validate(entry));
|
|
93
|
+
}
|
|
94
|
+
else if (entry.key === 'actions' || entry.key === 'not_actions') {
|
|
95
|
+
diagnostics = diagnostics.concat(actionValidator.validate(entry));
|
|
96
|
+
}
|
|
97
|
+
else if (entry.key === 'resources' || entry.key === 'not_resources') {
|
|
98
|
+
diagnostics = diagnostics.concat(resourceValidator.validate(entry));
|
|
99
|
+
}
|
|
100
|
+
else if (entry.key === 'condition') {
|
|
101
|
+
diagnostics = diagnostics.concat(conditionValidator.validate(entry));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
diagnostics.push(createDiagnostic(`Unrecognized entry "${entry.key}" in statement`, entry.keyRange));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!effectValidator.isValidated()) {
|
|
108
|
+
diagnostics.push(createDiagnostic(`Missing required "effect" entry in statement`, statement.range));
|
|
109
|
+
}
|
|
110
|
+
if (!actionValidator.isValidated()) {
|
|
111
|
+
diagnostics.push(createDiagnostic(`Missing required "actions" or "not_actions" entry in statement`, statement.range));
|
|
112
|
+
}
|
|
113
|
+
if (!resourceValidator.isValidated() && !principalValidator.isValidated()) {
|
|
114
|
+
diagnostics.push(createDiagnostic(`Missing required "resources"/"not_resources" or "principals"/"not_principals" entry in statement`, statement.range));
|
|
115
|
+
}
|
|
116
|
+
[sidValidator, effectValidator, principalValidator, actionValidator, resourceValidator, conditionValidator].forEach((x) => {
|
|
117
|
+
x.resetForStatement();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return diagnostics;
|
|
121
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
import { ElementValidator } from './base.ts';
|
|
4
|
+
export declare class EffectValidator extends ElementValidator {
|
|
5
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ElementValidator } from "./base.js";
|
|
2
|
+
import { createDiagnostic } from "./utils.js";
|
|
3
|
+
export class EffectValidator extends ElementValidator {
|
|
4
|
+
validate(entry) {
|
|
5
|
+
const diagnostics = super.validate(entry);
|
|
6
|
+
const value = entry.values[0]?.text;
|
|
7
|
+
if (value !== 'Allow' && value !== 'Deny') {
|
|
8
|
+
diagnostics.push(createDiagnostic(`effect value must be either "Allow" or "Deny"`, entry.valueRange));
|
|
9
|
+
}
|
|
10
|
+
return diagnostics;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
import { ElementValidator } from './base.ts';
|
|
4
|
+
export declare class PrincipalValidator extends ElementValidator {
|
|
5
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
import { ElementValidator } from './base.ts';
|
|
4
|
+
export declare class ResourceValidator extends ElementValidator {
|
|
5
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageclient';
|
|
2
|
+
import type { StatementEntry } from '../../lib/treesitter/base.ts';
|
|
3
|
+
import { ElementValidator } from './base.ts';
|
|
4
|
+
export declare class SidValidator extends ElementValidator {
|
|
5
|
+
#private;
|
|
6
|
+
constructor();
|
|
7
|
+
validate(entry: StatementEntry): Array<Diagnostic>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ElementValidator } from "./base.js";
|
|
2
|
+
import { createDiagnostic } from "./utils.js";
|
|
3
|
+
export class SidValidator extends ElementValidator {
|
|
4
|
+
#sids = {};
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.#sids = {};
|
|
8
|
+
}
|
|
9
|
+
validate(entry) {
|
|
10
|
+
const diagnostics = super.validate(entry);
|
|
11
|
+
const sidValue = entry.values[0]?.text;
|
|
12
|
+
if (!sidValue)
|
|
13
|
+
return [];
|
|
14
|
+
if (sidValue in this.#sids) {
|
|
15
|
+
diagnostics.push(createDiagnostic(`Duplicate statement id value "${sidValue}"`, entry.valueRange));
|
|
16
|
+
diagnostics.push(createDiagnostic(`Duplicate statement id value "${sidValue}"`, this.#sids[sidValue].valueRange));
|
|
17
|
+
}
|
|
18
|
+
this.#sids[sidValue] = entry;
|
|
19
|
+
return diagnostics;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createDiagnostic(message, range) {
|
|
2
|
+
return {
|
|
3
|
+
source: 'aws-iam-language-server',
|
|
4
|
+
message,
|
|
5
|
+
range: convertRangeToDiagnosticRange(range),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function convertRangeToDiagnosticRange(range) {
|
|
9
|
+
return {
|
|
10
|
+
start: {
|
|
11
|
+
character: range.start.column,
|
|
12
|
+
line: range.start.line,
|
|
13
|
+
},
|
|
14
|
+
end: {
|
|
15
|
+
character: range.end.column,
|
|
16
|
+
line: range.end.line,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Connection, DocumentLink, DocumentLinkParams, TextDocuments } from 'vscode-languageserver';
|
|
2
2
|
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
3
3
|
import type { TreeManager } from '../../lib/treesitter/manager.ts';
|
|
4
|
-
export declare function documentLinkHandler(params: DocumentLinkParams, documents: TextDocuments<TextDocument>,
|
|
4
|
+
export declare function documentLinkHandler(params: DocumentLinkParams, documents: TextDocuments<TextDocument>, treeManager: TreeManager, connection: Connection): DocumentLink[];
|
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
import { ServiceReference } from "../../lib/iam-policy/reference/services.js";
|
|
2
|
-
export function documentLinkHandler(params, documents,
|
|
2
|
+
export function documentLinkHandler(params, documents, treeManager, connection) {
|
|
3
3
|
const document = documents.get(params.textDocument.uri);
|
|
4
4
|
if (!document)
|
|
5
5
|
return [];
|
|
6
|
-
const
|
|
6
|
+
const handler = treeManager.getLanguageHandler(params.textDocument.uri);
|
|
7
|
+
if (!handler)
|
|
8
|
+
return [];
|
|
9
|
+
const policyDocuments = handler.getAllPolicyDocuments(params.textDocument.uri);
|
|
7
10
|
const links = [];
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
11
|
+
for (const policyDoc of policyDocuments) {
|
|
12
|
+
const actionKeys = policyDoc.policyFormat === 'hcl-block' ? new Set(['actions', 'not_actions']) : new Set(['Action', 'NotAction']);
|
|
13
|
+
for (const statement of policyDoc.statements) {
|
|
14
|
+
for (const entry of statement.entries) {
|
|
15
|
+
if (!actionKeys.has(entry.key))
|
|
16
|
+
continue;
|
|
17
|
+
for (const value of entry.values) {
|
|
18
|
+
const colonIndex = value.text.indexOf(':');
|
|
19
|
+
if (colonIndex === -1)
|
|
20
|
+
continue;
|
|
21
|
+
const action = ServiceReference.getAction(value.text);
|
|
22
|
+
if (!action)
|
|
23
|
+
continue;
|
|
24
|
+
const start = { line: value.range.start.line, character: value.range.start.column };
|
|
25
|
+
const end = { line: value.range.end.line, character: value.range.end.column };
|
|
26
|
+
const range = { start, end };
|
|
27
|
+
if (action.iamUrl) {
|
|
28
|
+
links.push({
|
|
29
|
+
range,
|
|
30
|
+
target: action.iamUrl,
|
|
31
|
+
tooltip: 'IAM Actions, Conditions, and Context Keys',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (action.operationUrl) {
|
|
35
|
+
links.push({
|
|
36
|
+
range,
|
|
37
|
+
target: action.operationUrl,
|
|
38
|
+
tooltip: 'API Operation Documentation',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
connection.console.debug(`Found ${links.length} document links in ${params.textDocument.uri}`);
|
|
@@ -11,7 +11,7 @@ export declare class ServiceReference {
|
|
|
11
11
|
types: string[];
|
|
12
12
|
}>;
|
|
13
13
|
static getGlobalConditionKeys(): Array<GlobalConditionKey>;
|
|
14
|
-
static getAction(
|
|
14
|
+
static getAction(action: string): Action | undefined;
|
|
15
15
|
static getConditionKey(service: string, keyName: string): ConditionKey | undefined;
|
|
16
16
|
static getResourcesForActions(actions: string[]): Map<string, {
|
|
17
17
|
service: string;
|
|
@@ -60,9 +60,11 @@ export class ServiceReference {
|
|
|
60
60
|
}
|
|
61
61
|
return ServiceReference.#globalConditionKeys;
|
|
62
62
|
}
|
|
63
|
-
static getAction(
|
|
63
|
+
static getAction(action) {
|
|
64
64
|
try {
|
|
65
|
-
|
|
65
|
+
const serviceName = action.split(':')[0];
|
|
66
|
+
const actionName = action.split(':')[1];
|
|
67
|
+
return ServiceReference.getServiceData(serviceName).actions[actionName];
|
|
66
68
|
}
|
|
67
69
|
catch {
|
|
68
70
|
return undefined;
|
|
@@ -1,8 +1,33 @@
|
|
|
1
|
-
import type { Language, Tree } from 'web-tree-sitter';
|
|
1
|
+
import type { Language, Node, Tree } from 'web-tree-sitter';
|
|
2
2
|
export type Position = {
|
|
3
3
|
line: number;
|
|
4
4
|
column: number;
|
|
5
5
|
};
|
|
6
|
+
export type Range = {
|
|
7
|
+
start: Position;
|
|
8
|
+
end: Position;
|
|
9
|
+
};
|
|
10
|
+
export type StatementValue = {
|
|
11
|
+
text: string;
|
|
12
|
+
range: Range;
|
|
13
|
+
};
|
|
14
|
+
export type StatementEntry = {
|
|
15
|
+
key: string;
|
|
16
|
+
keyRange: Range;
|
|
17
|
+
values: StatementValue[];
|
|
18
|
+
valueRange: Range;
|
|
19
|
+
children?: StatementEntry[];
|
|
20
|
+
};
|
|
21
|
+
export type StatementNode = {
|
|
22
|
+
range: Range;
|
|
23
|
+
entries: StatementEntry[];
|
|
24
|
+
};
|
|
25
|
+
export type PolicyDocumentNode = {
|
|
26
|
+
range: Range;
|
|
27
|
+
policyFormat: PolicyFormat;
|
|
28
|
+
statements: StatementNode[];
|
|
29
|
+
};
|
|
30
|
+
export declare function nodeRange(node: Node): Range;
|
|
6
31
|
export type PolicyFormat = 'standard' | 'hcl-block';
|
|
7
32
|
export type CursorContext = {
|
|
8
33
|
keys: string[];
|
|
@@ -29,8 +54,9 @@ export declare class TreeBase {
|
|
|
29
54
|
openDocument(uri: string, content: string): void;
|
|
30
55
|
updateDocument(uri: string, content: string): void;
|
|
31
56
|
closeDocument(uri: string): void;
|
|
32
|
-
getNodeAtPosition(uri: string, position: Position):
|
|
57
|
+
getNodeAtPosition(uri: string, position: Position): Node | null;
|
|
33
58
|
getCursorContext(_uri: string, _position: Position): CursorContext | null;
|
|
34
59
|
getStatementContext(_uri: string, _position: Position): StatementContext | null;
|
|
35
60
|
getSiblingKeys(_uri: string, _position: Position): string[];
|
|
61
|
+
getAllPolicyDocuments(_uri: string): PolicyDocumentNode[];
|
|
36
62
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Parser } from 'web-tree-sitter';
|
|
2
2
|
await Parser.init();
|
|
3
|
+
export function nodeRange(node) {
|
|
4
|
+
return {
|
|
5
|
+
start: { line: node.startPosition.row, column: node.startPosition.column },
|
|
6
|
+
end: { line: node.endPosition.row, column: node.endPosition.column },
|
|
7
|
+
};
|
|
8
|
+
}
|
|
3
9
|
export class TreeBase {
|
|
4
10
|
#trees = new Map();
|
|
5
11
|
#parser;
|
|
@@ -47,4 +53,7 @@ export class TreeBase {
|
|
|
47
53
|
getSiblingKeys(_uri, _position) {
|
|
48
54
|
throw new Error('getSiblingKeys must be implemented by a subclass');
|
|
49
55
|
}
|
|
56
|
+
getAllPolicyDocuments(_uri) {
|
|
57
|
+
throw new Error('getAllPolicyDocuments must be implemented by a subclass');
|
|
58
|
+
}
|
|
50
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CursorContext, Position, StatementContext } from './base.ts';
|
|
1
|
+
import type { CursorContext, PolicyDocumentNode, Position, StatementContext } from './base.ts';
|
|
2
2
|
import { TreeBase } from './base.ts';
|
|
3
3
|
export declare class TreeHcl extends TreeBase {
|
|
4
4
|
#private;
|
|
@@ -6,4 +6,5 @@ export declare class TreeHcl extends TreeBase {
|
|
|
6
6
|
getCursorContext(uri: string, position: Position): CursorContext | null;
|
|
7
7
|
getStatementContext(uri: string, position: Position): StatementContext | null;
|
|
8
8
|
getSiblingKeys(uri: string, position: Position): string[];
|
|
9
|
+
getAllPolicyDocuments(uri: string): PolicyDocumentNode[];
|
|
9
10
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { Language } from 'web-tree-sitter';
|
|
3
|
-
import { TreeBase } from "./base.js";
|
|
3
|
+
import { nodeRange, TreeBase } from "./base.js";
|
|
4
4
|
export class TreeHcl extends TreeBase {
|
|
5
5
|
static async init() {
|
|
6
6
|
const grammarDir = resolve(import.meta.dirname, '../../grammars');
|
|
@@ -65,6 +65,267 @@ export class TreeHcl extends TreeBase {
|
|
|
65
65
|
}
|
|
66
66
|
return [];
|
|
67
67
|
}
|
|
68
|
+
getAllPolicyDocuments(uri) {
|
|
69
|
+
const tree = this.getTree(uri);
|
|
70
|
+
if (!tree)
|
|
71
|
+
return [];
|
|
72
|
+
const root = tree.rootNode;
|
|
73
|
+
const results = [];
|
|
74
|
+
// Block mode: statement { ... } — group by parent data block's body
|
|
75
|
+
const blocks = this.#findAllStatementBlocks(root);
|
|
76
|
+
const blockGroups = new Map();
|
|
77
|
+
for (const block of blocks) {
|
|
78
|
+
const parentBody = block.parent;
|
|
79
|
+
if (!parentBody || parentBody.type !== 'body')
|
|
80
|
+
continue;
|
|
81
|
+
const existing = blockGroups.get(parentBody.id);
|
|
82
|
+
if (existing) {
|
|
83
|
+
existing.blocks.push(block);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
blockGroups.set(parentBody.id, { parentBody, blocks: [block] });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const group of blockGroups.values()) {
|
|
90
|
+
results.push({
|
|
91
|
+
range: nodeRange(group.parentBody),
|
|
92
|
+
policyFormat: 'hcl-block',
|
|
93
|
+
statements: group.blocks.map((block) => {
|
|
94
|
+
const body = block.namedChildren.find((child) => child.type === 'body');
|
|
95
|
+
return {
|
|
96
|
+
range: nodeRange(block),
|
|
97
|
+
entries: body ? this.#buildBlockStatementEntries(body) : [],
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Jsonencode mode: jsonencode({ Statement = [...] }) — group by parent tuple
|
|
103
|
+
const objects = this.#findAllJsonencodeStatementObjects(root);
|
|
104
|
+
const tupleGroups = new Map();
|
|
105
|
+
for (const object of objects) {
|
|
106
|
+
const tuple = this.#findParentTuple(object);
|
|
107
|
+
if (!tuple)
|
|
108
|
+
continue;
|
|
109
|
+
const existing = tupleGroups.get(tuple.id);
|
|
110
|
+
if (existing) {
|
|
111
|
+
existing.objects.push(object);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
tupleGroups.set(tuple.id, { tuple, objects: [object] });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const group of tupleGroups.values()) {
|
|
118
|
+
results.push({
|
|
119
|
+
range: nodeRange(group.tuple),
|
|
120
|
+
policyFormat: 'standard',
|
|
121
|
+
statements: group.objects.map((object) => ({
|
|
122
|
+
range: nodeRange(object),
|
|
123
|
+
entries: this.#buildJsonencodeStatementEntries(object),
|
|
124
|
+
})),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
#findParentTuple(object) {
|
|
130
|
+
let current = object.parent;
|
|
131
|
+
while (current && (current.type === 'collection_value' || current.type === 'expression')) {
|
|
132
|
+
current = current.parent;
|
|
133
|
+
}
|
|
134
|
+
return current?.type === 'tuple' ? current : null;
|
|
135
|
+
}
|
|
136
|
+
#findAllStatementBlocks(root) {
|
|
137
|
+
const results = [];
|
|
138
|
+
const visit = (node) => {
|
|
139
|
+
if (node.type === 'block' && this.#getBlockIdentifier(node) === 'statement') {
|
|
140
|
+
results.push(node);
|
|
141
|
+
return; // skip descendants
|
|
142
|
+
}
|
|
143
|
+
for (const child of node.namedChildren) {
|
|
144
|
+
visit(child);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
visit(root);
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
#findAllJsonencodeStatementObjects(root) {
|
|
151
|
+
const results = [];
|
|
152
|
+
const visit = (node) => {
|
|
153
|
+
if (node.type === 'object' && this.#isInsideStatementTuple(node)) {
|
|
154
|
+
results.push(node);
|
|
155
|
+
return; // skip descendants
|
|
156
|
+
}
|
|
157
|
+
for (const child of node.namedChildren) {
|
|
158
|
+
visit(child);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
visit(root);
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
#buildBlockStatementEntries(body) {
|
|
165
|
+
const entries = [];
|
|
166
|
+
for (const child of body.namedChildren) {
|
|
167
|
+
if (child.type === 'attribute') {
|
|
168
|
+
const id = child.namedChildren.find((c) => c.type === 'identifier');
|
|
169
|
+
if (!id)
|
|
170
|
+
continue;
|
|
171
|
+
const keyRange = nodeRange(id);
|
|
172
|
+
const expression = child.namedChildren.find((c) => c.type === 'expression');
|
|
173
|
+
const values = expression ? this.#readExpressionStatementValues(expression) : [];
|
|
174
|
+
const valueRange = expression
|
|
175
|
+
? nodeRange(expression)
|
|
176
|
+
: {
|
|
177
|
+
start: { line: child.endPosition.row, column: child.endPosition.column },
|
|
178
|
+
end: { line: child.endPosition.row, column: child.endPosition.column },
|
|
179
|
+
};
|
|
180
|
+
entries.push({ key: id.text, keyRange, values, valueRange });
|
|
181
|
+
}
|
|
182
|
+
else if (child.type === 'block') {
|
|
183
|
+
const blockId = this.#getBlockIdentifier(child);
|
|
184
|
+
if (!blockId)
|
|
185
|
+
continue;
|
|
186
|
+
const keyIdNode = child.namedChildren.find((c) => c.type === 'identifier');
|
|
187
|
+
const keyRange = keyIdNode ? nodeRange(keyIdNode) : nodeRange(child);
|
|
188
|
+
const blockBody = child.namedChildren.find((c) => c.type === 'body');
|
|
189
|
+
const children = blockBody ? this.#buildBlockStatementEntries(blockBody) : [];
|
|
190
|
+
const valueRange = blockBody ? nodeRange(blockBody) : nodeRange(child);
|
|
191
|
+
entries.push({ key: blockId, keyRange, values: [], valueRange, children });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return entries;
|
|
195
|
+
}
|
|
196
|
+
#readExpressionStatementValues(expression) {
|
|
197
|
+
// Single string literal
|
|
198
|
+
const literal = expression.namedChildren.find((child) => child.type === 'literal_value');
|
|
199
|
+
if (literal) {
|
|
200
|
+
const string = literal.namedChildren.find((child) => child.type === 'string_lit');
|
|
201
|
+
const template = string?.namedChildren.find((child) => child.type === 'template_literal');
|
|
202
|
+
if (template?.text)
|
|
203
|
+
return [{ text: template.text, range: nodeRange(template) }];
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
// Tuple of strings
|
|
207
|
+
let tuple = expression.namedChildren.find((child) => child.type === 'tuple');
|
|
208
|
+
if (!tuple) {
|
|
209
|
+
const collectionValue = expression.namedChildren.find((child) => child.type === 'collection_value');
|
|
210
|
+
tuple = collectionValue?.namedChildren.find((child) => child.type === 'tuple') ?? undefined;
|
|
211
|
+
}
|
|
212
|
+
if (tuple) {
|
|
213
|
+
const values = [];
|
|
214
|
+
for (const element of tuple.namedChildren) {
|
|
215
|
+
const elementExpression = element.type === 'expression' ? element : null;
|
|
216
|
+
const elementLiteral = elementExpression?.namedChildren.find((child) => child.type === 'literal_value');
|
|
217
|
+
const elementString = elementLiteral?.namedChildren.find((child) => child.type === 'string_lit');
|
|
218
|
+
const elementTemplate = elementString?.namedChildren.find((child) => child.type === 'template_literal');
|
|
219
|
+
if (elementTemplate?.text)
|
|
220
|
+
values.push({ text: elementTemplate.text, range: nodeRange(elementTemplate) });
|
|
221
|
+
}
|
|
222
|
+
return values;
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
#buildJsonencodeStatementEntries(object) {
|
|
227
|
+
const entries = [];
|
|
228
|
+
for (const child of object.namedChildren) {
|
|
229
|
+
if (child.type !== 'object_elem')
|
|
230
|
+
continue;
|
|
231
|
+
const key = this.#getObjectElemKey(child);
|
|
232
|
+
if (!key)
|
|
233
|
+
continue;
|
|
234
|
+
const keyExpression = child.namedChildren.find((c) => c.type === 'expression');
|
|
235
|
+
const keyId = keyExpression?.namedChildren
|
|
236
|
+
.find((c) => c.type === 'variable_expr')
|
|
237
|
+
?.namedChildren.find((c) => c.type === 'identifier');
|
|
238
|
+
const keyRange = keyId ? nodeRange(keyId) : nodeRange(child);
|
|
239
|
+
const expressions = child.namedChildren.filter((c) => c.type === 'expression');
|
|
240
|
+
const valueExpression = expressions.length >= 2 ? expressions[1] : null;
|
|
241
|
+
const values = valueExpression ? this.#readJsonencodeExpressionStatementValues(valueExpression) : [];
|
|
242
|
+
const valueRange = valueExpression
|
|
243
|
+
? nodeRange(valueExpression)
|
|
244
|
+
: {
|
|
245
|
+
start: { line: child.endPosition.row, column: child.endPosition.column },
|
|
246
|
+
end: { line: child.endPosition.row, column: child.endPosition.column },
|
|
247
|
+
};
|
|
248
|
+
const nestedKeys = new Set(['Condition', 'Principal', 'NotPrincipal']);
|
|
249
|
+
let children;
|
|
250
|
+
if (nestedKeys.has(key) && valueExpression) {
|
|
251
|
+
let nestedObject = valueExpression.namedChildren.find((c) => c.type === 'object');
|
|
252
|
+
if (!nestedObject) {
|
|
253
|
+
const collectionValue = valueExpression.namedChildren.find((c) => c.type === 'collection_value');
|
|
254
|
+
nestedObject = collectionValue?.namedChildren.find((c) => c.type === 'object') ?? undefined;
|
|
255
|
+
}
|
|
256
|
+
if (nestedObject) {
|
|
257
|
+
children = this.#buildJsonencodeNestedEntries(nestedObject);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
entries.push({ key, keyRange, values, valueRange, ...(children ? { children } : {}) });
|
|
261
|
+
}
|
|
262
|
+
return entries;
|
|
263
|
+
}
|
|
264
|
+
#readJsonencodeExpressionStatementValues(expression) {
|
|
265
|
+
// Single string literal
|
|
266
|
+
const literal = expression.namedChildren.find((child) => child.type === 'literal_value');
|
|
267
|
+
if (literal) {
|
|
268
|
+
const string = literal.namedChildren.find((child) => child.type === 'string_lit');
|
|
269
|
+
const template = string?.namedChildren.find((child) => child.type === 'template_literal');
|
|
270
|
+
if (template?.text)
|
|
271
|
+
return [{ text: template.text, range: nodeRange(template) }];
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
// Tuple
|
|
275
|
+
let tuple = expression.namedChildren.find((child) => child.type === 'tuple');
|
|
276
|
+
if (!tuple) {
|
|
277
|
+
const collectionValue = expression.namedChildren.find((child) => child.type === 'collection_value');
|
|
278
|
+
tuple = collectionValue?.namedChildren.find((child) => child.type === 'tuple') ?? undefined;
|
|
279
|
+
}
|
|
280
|
+
if (tuple) {
|
|
281
|
+
const values = [];
|
|
282
|
+
for (const element of tuple.namedChildren) {
|
|
283
|
+
const elementExpression = element.type === 'expression' ? element : null;
|
|
284
|
+
const elementLiteral = elementExpression?.namedChildren.find((child) => child.type === 'literal_value');
|
|
285
|
+
const elementString = elementLiteral?.namedChildren.find((child) => child.type === 'string_lit');
|
|
286
|
+
const elementTemplate = elementString?.namedChildren.find((child) => child.type === 'template_literal');
|
|
287
|
+
if (elementTemplate?.text)
|
|
288
|
+
values.push({ text: elementTemplate.text, range: nodeRange(elementTemplate) });
|
|
289
|
+
}
|
|
290
|
+
return values;
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
#buildJsonencodeNestedEntries(object) {
|
|
295
|
+
const entries = [];
|
|
296
|
+
for (const child of object.namedChildren) {
|
|
297
|
+
if (child.type !== 'object_elem')
|
|
298
|
+
continue;
|
|
299
|
+
const key = this.#getObjectElemKey(child);
|
|
300
|
+
if (!key)
|
|
301
|
+
continue;
|
|
302
|
+
const keyExpression = child.namedChildren.find((c) => c.type === 'expression');
|
|
303
|
+
const keyId = keyExpression?.namedChildren
|
|
304
|
+
.find((c) => c.type === 'variable_expr')
|
|
305
|
+
?.namedChildren.find((c) => c.type === 'identifier');
|
|
306
|
+
const keyRange = keyId ? nodeRange(keyId) : nodeRange(child);
|
|
307
|
+
const expressions = child.namedChildren.filter((c) => c.type === 'expression');
|
|
308
|
+
const valueExpression = expressions.length >= 2 ? expressions[1] : null;
|
|
309
|
+
const values = valueExpression ? this.#readJsonencodeExpressionStatementValues(valueExpression) : [];
|
|
310
|
+
const valueRange = valueExpression
|
|
311
|
+
? nodeRange(valueExpression)
|
|
312
|
+
: {
|
|
313
|
+
start: { line: child.endPosition.row, column: child.endPosition.column },
|
|
314
|
+
end: { line: child.endPosition.row, column: child.endPosition.column },
|
|
315
|
+
};
|
|
316
|
+
let nestedObject;
|
|
317
|
+
if (valueExpression) {
|
|
318
|
+
nestedObject = valueExpression.namedChildren.find((c) => c.type === 'object');
|
|
319
|
+
if (!nestedObject) {
|
|
320
|
+
const collectionValue = valueExpression.namedChildren.find((c) => c.type === 'collection_value');
|
|
321
|
+
nestedObject = collectionValue?.namedChildren.find((c) => c.type === 'object') ?? undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const children = nestedObject ? this.#buildJsonencodeNestedEntries(nestedObject) : undefined;
|
|
325
|
+
entries.push({ key, keyRange, values, valueRange, ...(children ? { children } : {}) });
|
|
326
|
+
}
|
|
327
|
+
return entries;
|
|
328
|
+
}
|
|
68
329
|
// ---------------------------------------------------------------------------
|
|
69
330
|
// Jsonencode mode — object/object_elem/tuple, PascalCase keys, same as JSON
|
|
70
331
|
// ---------------------------------------------------------------------------
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CursorContext, Position, StatementContext } from './base.ts';
|
|
1
|
+
import type { CursorContext, PolicyDocumentNode, Position, StatementContext } from './base.ts';
|
|
2
2
|
import { TreeBase } from './base.ts';
|
|
3
3
|
export declare class TreeJson extends TreeBase {
|
|
4
4
|
#private;
|
|
@@ -6,4 +6,5 @@ export declare class TreeJson extends TreeBase {
|
|
|
6
6
|
getCursorContext(uri: string, position: Position): CursorContext | null;
|
|
7
7
|
getStatementContext(uri: string, position: Position): StatementContext | null;
|
|
8
8
|
getSiblingKeys(uri: string, position: Position): string[];
|
|
9
|
+
getAllPolicyDocuments(uri: string): PolicyDocumentNode[];
|
|
9
10
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { Language } from 'web-tree-sitter';
|
|
3
|
-
import { TreeBase } from "./base.js";
|
|
3
|
+
import { nodeRange, TreeBase } from "./base.js";
|
|
4
4
|
export class TreeJson extends TreeBase {
|
|
5
5
|
static async init() {
|
|
6
6
|
const grammarDir = resolve(import.meta.dirname, '../../grammars');
|
|
@@ -59,6 +59,131 @@ export class TreeJson extends TreeBase {
|
|
|
59
59
|
return [];
|
|
60
60
|
return this.#collectExistingKeys(this.#findInnermostObject(node, statementObject));
|
|
61
61
|
}
|
|
62
|
+
getAllPolicyDocuments(uri) {
|
|
63
|
+
const tree = this.getTree(uri);
|
|
64
|
+
if (!tree)
|
|
65
|
+
return [];
|
|
66
|
+
const objects = this.#findAllStatementObjects(tree.rootNode);
|
|
67
|
+
// Group by parent array (each Statement array = one policy document)
|
|
68
|
+
const groups = new Map();
|
|
69
|
+
for (const object of objects) {
|
|
70
|
+
const array = object.parent;
|
|
71
|
+
if (!array || array.type !== 'array')
|
|
72
|
+
continue;
|
|
73
|
+
const existing = groups.get(array.id);
|
|
74
|
+
if (existing) {
|
|
75
|
+
existing.objects.push(object);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
groups.set(array.id, { array, objects: [object] });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const group of groups.values()) {
|
|
83
|
+
results.push({
|
|
84
|
+
range: nodeRange(group.array),
|
|
85
|
+
policyFormat: 'standard',
|
|
86
|
+
statements: group.objects.map((object) => ({
|
|
87
|
+
range: nodeRange(object),
|
|
88
|
+
entries: this.#buildStatementEntries(object),
|
|
89
|
+
})),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
#findAllStatementObjects(root) {
|
|
95
|
+
const results = [];
|
|
96
|
+
const visit = (node) => {
|
|
97
|
+
if (node.type === 'object' && this.#isInsideStatementArray(node)) {
|
|
98
|
+
results.push(node);
|
|
99
|
+
return; // skip descendants
|
|
100
|
+
}
|
|
101
|
+
for (const child of node.namedChildren) {
|
|
102
|
+
visit(child);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
visit(root);
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
#buildStatementEntries(object) {
|
|
109
|
+
const entries = [];
|
|
110
|
+
for (const child of object.namedChildren) {
|
|
111
|
+
if (child.type !== 'pair')
|
|
112
|
+
continue;
|
|
113
|
+
const key = this.#getPairKeyText(child);
|
|
114
|
+
if (!key)
|
|
115
|
+
continue;
|
|
116
|
+
const keyString = child.namedChildren[0];
|
|
117
|
+
const keyRange = keyString ? nodeRange(keyString) : nodeRange(child);
|
|
118
|
+
const values = this.#readPairStatementValues(child);
|
|
119
|
+
const valueRange = this.#getPairValueRange(child);
|
|
120
|
+
const nestedKeys = new Set(['Condition', 'Principal', 'NotPrincipal']);
|
|
121
|
+
let children;
|
|
122
|
+
if (nestedKeys.has(key)) {
|
|
123
|
+
const valueNode = child.namedChildren[1];
|
|
124
|
+
if (valueNode?.type === 'object') {
|
|
125
|
+
children = this.#buildNestedEntries(valueNode);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
entries.push({ key, keyRange, values, valueRange, ...(children ? { children } : {}) });
|
|
129
|
+
}
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
#buildNestedEntries(object) {
|
|
133
|
+
const entries = [];
|
|
134
|
+
for (const child of object.namedChildren) {
|
|
135
|
+
if (child.type !== 'pair')
|
|
136
|
+
continue;
|
|
137
|
+
const key = this.#getPairKeyText(child);
|
|
138
|
+
if (!key)
|
|
139
|
+
continue;
|
|
140
|
+
const keyString = child.namedChildren[0];
|
|
141
|
+
const keyRange = keyString ? nodeRange(keyString) : nodeRange(child);
|
|
142
|
+
const values = this.#readPairStatementValues(child);
|
|
143
|
+
const valueRange = this.#getPairValueRange(child);
|
|
144
|
+
const valueNode = child.namedChildren[1];
|
|
145
|
+
let children;
|
|
146
|
+
if (valueNode?.type === 'object') {
|
|
147
|
+
children = this.#buildNestedEntries(valueNode);
|
|
148
|
+
}
|
|
149
|
+
entries.push({ key, keyRange, values, valueRange, ...(children ? { children } : {}) });
|
|
150
|
+
}
|
|
151
|
+
return entries;
|
|
152
|
+
}
|
|
153
|
+
#readPairStatementValues(pair) {
|
|
154
|
+
const valueNode = pair.namedChildren[1];
|
|
155
|
+
if (!valueNode)
|
|
156
|
+
return [];
|
|
157
|
+
if (valueNode.type === 'string') {
|
|
158
|
+
const content = valueNode.namedChildren.find((child) => child.type === 'string_content');
|
|
159
|
+
if (!content?.text)
|
|
160
|
+
return [];
|
|
161
|
+
return [{ text: content.text, range: nodeRange(content) }];
|
|
162
|
+
}
|
|
163
|
+
if (valueNode.type === 'array') {
|
|
164
|
+
const values = [];
|
|
165
|
+
for (const element of valueNode.namedChildren) {
|
|
166
|
+
if (element.type !== 'string')
|
|
167
|
+
continue;
|
|
168
|
+
const content = element.namedChildren.find((child) => child.type === 'string_content');
|
|
169
|
+
if (content?.text) {
|
|
170
|
+
values.push({ text: content.text, range: nodeRange(content) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return values;
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
#getPairValueRange(pair) {
|
|
178
|
+
const valueNode = pair.namedChildren[1];
|
|
179
|
+
if (!valueNode) {
|
|
180
|
+
return {
|
|
181
|
+
start: { line: pair.endPosition.row, column: pair.endPosition.column },
|
|
182
|
+
end: { line: pair.endPosition.row, column: pair.endPosition.column },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return nodeRange(valueNode);
|
|
186
|
+
}
|
|
62
187
|
#resolveCursorContext(node, statementObject, position) {
|
|
63
188
|
const cursorPath = this.#buildCursorPath(node, statementObject, position);
|
|
64
189
|
return { ...cursorPath, policyFormat: 'standard' };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CursorContext, Position, StatementContext } from './base.ts';
|
|
1
|
+
import type { CursorContext, PolicyDocumentNode, Position, StatementContext } from './base.ts';
|
|
2
2
|
import { TreeBase } from './base.ts';
|
|
3
3
|
export declare class TreeYaml extends TreeBase {
|
|
4
4
|
#private;
|
|
@@ -6,4 +6,5 @@ export declare class TreeYaml extends TreeBase {
|
|
|
6
6
|
getCursorContext(uri: string, position: Position): CursorContext | null;
|
|
7
7
|
getStatementContext(uri: string, position: Position): StatementContext | null;
|
|
8
8
|
getSiblingKeys(uri: string, position: Position): string[];
|
|
9
|
+
getAllPolicyDocuments(uri: string): PolicyDocumentNode[];
|
|
9
10
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { Language } from 'web-tree-sitter';
|
|
3
|
-
import { TreeBase } from "./base.js";
|
|
3
|
+
import { nodeRange, TreeBase } from "./base.js";
|
|
4
4
|
export class TreeYaml extends TreeBase {
|
|
5
5
|
static async init() {
|
|
6
6
|
const grammarDir = resolve(import.meta.dirname, '../../grammars');
|
|
@@ -61,6 +61,140 @@ export class TreeYaml extends TreeBase {
|
|
|
61
61
|
return [];
|
|
62
62
|
return this.#collectExistingKeysAtPath(match.mapping, cursorPath.keys);
|
|
63
63
|
}
|
|
64
|
+
getAllPolicyDocuments(uri) {
|
|
65
|
+
const root = this.getTree(uri)?.rootNode;
|
|
66
|
+
if (!root)
|
|
67
|
+
return [];
|
|
68
|
+
const mappings = this.#findAllStatementMappings(root);
|
|
69
|
+
// Group by parent block_sequence (each sequence = one policy document)
|
|
70
|
+
const groups = new Map();
|
|
71
|
+
for (const mapping of mappings) {
|
|
72
|
+
const sequence = this.#getParentStatementSequence(mapping);
|
|
73
|
+
if (!sequence)
|
|
74
|
+
continue;
|
|
75
|
+
const existing = groups.get(sequence.id);
|
|
76
|
+
if (existing) {
|
|
77
|
+
existing.mappings.push(mapping);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
groups.set(sequence.id, { sequence, mappings: [mapping] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const group of groups.values()) {
|
|
85
|
+
results.push({
|
|
86
|
+
range: nodeRange(group.sequence),
|
|
87
|
+
policyFormat: 'standard',
|
|
88
|
+
statements: group.mappings.map((mapping) => ({
|
|
89
|
+
range: nodeRange(mapping),
|
|
90
|
+
entries: this.#buildStatementEntries(mapping),
|
|
91
|
+
})),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
#buildStatementEntries(mapping) {
|
|
97
|
+
const entries = [];
|
|
98
|
+
for (const child of mapping.namedChildren) {
|
|
99
|
+
if (child.type !== 'block_mapping_pair')
|
|
100
|
+
continue;
|
|
101
|
+
const key = this.#getPairKeyText(child);
|
|
102
|
+
if (!key)
|
|
103
|
+
continue;
|
|
104
|
+
const keyFlow = child.namedChildren.find((c) => c.type === 'flow_node');
|
|
105
|
+
const keyRange = keyFlow ? nodeRange(keyFlow) : nodeRange(child);
|
|
106
|
+
const values = this.#readPairStatementValues(child);
|
|
107
|
+
const valueRange = this.#getPairValueRange(child);
|
|
108
|
+
const nestedKeys = new Set(['Condition', 'Principal', 'NotPrincipal']);
|
|
109
|
+
let children;
|
|
110
|
+
if (nestedKeys.has(key)) {
|
|
111
|
+
const nestedMapping = this.#findValueBlockMapping(child);
|
|
112
|
+
if (nestedMapping) {
|
|
113
|
+
children = this.#buildNestedEntries(nestedMapping);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
entries.push({ key, keyRange, values, valueRange, ...(children ? { children } : {}) });
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
#buildNestedEntries(mapping) {
|
|
121
|
+
const entries = [];
|
|
122
|
+
for (const child of mapping.namedChildren) {
|
|
123
|
+
if (child.type !== 'block_mapping_pair')
|
|
124
|
+
continue;
|
|
125
|
+
const key = this.#getPairKeyText(child);
|
|
126
|
+
if (!key)
|
|
127
|
+
continue;
|
|
128
|
+
const keyFlow = child.namedChildren.find((c) => c.type === 'flow_node');
|
|
129
|
+
const keyRange = keyFlow ? nodeRange(keyFlow) : nodeRange(child);
|
|
130
|
+
const values = this.#readPairStatementValues(child);
|
|
131
|
+
const valueRange = this.#getPairValueRange(child);
|
|
132
|
+
// Recurse for deeper nesting (Condition operator → key → values)
|
|
133
|
+
const nestedMapping = this.#findValueBlockMapping(child);
|
|
134
|
+
let children;
|
|
135
|
+
if (nestedMapping) {
|
|
136
|
+
children = this.#buildNestedEntries(nestedMapping);
|
|
137
|
+
}
|
|
138
|
+
entries.push({ key, keyRange, values, valueRange, ...(children ? { children } : {}) });
|
|
139
|
+
}
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
#readPairStatementValues(pair) {
|
|
143
|
+
if (pair.namedChildren.length < 2)
|
|
144
|
+
return [];
|
|
145
|
+
const valueNode = pair.namedChildren[1];
|
|
146
|
+
// Direct flow_node scalar
|
|
147
|
+
if (valueNode.type === 'flow_node') {
|
|
148
|
+
return this.#flowNodeToStatementValue(valueNode);
|
|
149
|
+
}
|
|
150
|
+
// block_node wrapping
|
|
151
|
+
const inner = valueNode.type === 'block_node' ? (valueNode.namedChildren[0] ?? null) : valueNode;
|
|
152
|
+
if (!inner)
|
|
153
|
+
return [];
|
|
154
|
+
if (inner.type === 'flow_node') {
|
|
155
|
+
return this.#flowNodeToStatementValue(inner);
|
|
156
|
+
}
|
|
157
|
+
// Block sequence
|
|
158
|
+
if (inner.type === 'block_sequence') {
|
|
159
|
+
const values = [];
|
|
160
|
+
for (const item of inner.namedChildren) {
|
|
161
|
+
if (item.type !== 'block_sequence_item')
|
|
162
|
+
continue;
|
|
163
|
+
const flowNode = item.namedChildren.find((child) => child.type === 'flow_node');
|
|
164
|
+
if (flowNode) {
|
|
165
|
+
values.push(...this.#flowNodeToStatementValue(flowNode));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return values;
|
|
169
|
+
}
|
|
170
|
+
// Flow sequence
|
|
171
|
+
if (inner.type === 'flow_sequence') {
|
|
172
|
+
const values = [];
|
|
173
|
+
for (const flowItem of inner.namedChildren) {
|
|
174
|
+
if (flowItem.type !== 'flow_node')
|
|
175
|
+
continue;
|
|
176
|
+
values.push(...this.#flowNodeToStatementValue(flowItem));
|
|
177
|
+
}
|
|
178
|
+
return values;
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
#flowNodeToStatementValue(flowNode) {
|
|
183
|
+
const text = this.#getScalarText(flowNode);
|
|
184
|
+
if (text === null)
|
|
185
|
+
return [];
|
|
186
|
+
return [{ text, range: nodeRange(flowNode) }];
|
|
187
|
+
}
|
|
188
|
+
#getPairValueRange(pair) {
|
|
189
|
+
if (pair.namedChildren.length < 2) {
|
|
190
|
+
// No value — zero-width range at key end
|
|
191
|
+
return {
|
|
192
|
+
start: { line: pair.endPosition.row, column: pair.endPosition.column },
|
|
193
|
+
end: { line: pair.endPosition.row, column: pair.endPosition.column },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return nodeRange(pair.namedChildren[1]);
|
|
197
|
+
}
|
|
64
198
|
/**
|
|
65
199
|
* Traverse the tree from root, collecting every block_mapping that sits inside
|
|
66
200
|
* a Statement sequence. Skip descendants of matched mappings (they can't nest).
|
package/src/server.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createConnection, ProposedFeatures, TextDocumentSyncKind, TextDocuments } from 'vscode-languageserver/node.js';
|
|
3
3
|
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
4
4
|
import { handleCompletionRequest } from "./handlers/completion/index.js";
|
|
5
|
+
import { diagnosticsHandler } from "./handlers/diagnostics/diagnostics.js";
|
|
5
6
|
import { documentLinkHandler } from "./handlers/document-link/document-link.js";
|
|
6
7
|
import { TreeManager } from "./lib/treesitter/manager.js";
|
|
7
8
|
const connection = createConnection(ProposedFeatures.all);
|
|
@@ -20,9 +21,17 @@ connection.onInitialize(async () => {
|
|
|
20
21
|
},
|
|
21
22
|
};
|
|
22
23
|
});
|
|
23
|
-
documents.onDidOpen(({ document }) =>
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
documents.onDidOpen(async ({ document }) => {
|
|
25
|
+
await treeManager.openDocument(document.uri, document.getText(), document.languageId);
|
|
26
|
+
await diagnosticsHandler(document, treeManager, connection);
|
|
27
|
+
});
|
|
28
|
+
documents.onDidChangeContent(async ({ document }) => {
|
|
29
|
+
await treeManager.updateDocument(document.uri, document.getText());
|
|
30
|
+
await diagnosticsHandler(document, treeManager, connection);
|
|
31
|
+
});
|
|
32
|
+
documents.onDidClose(async ({ document }) => {
|
|
33
|
+
await treeManager.closeDocument(document.uri);
|
|
34
|
+
});
|
|
26
35
|
connection.onCompletion((params) => handleCompletionRequest(params, documents, treeManager, connection));
|
|
27
36
|
connection.onDocumentLinks((params) => documentLinkHandler(params, documents, treeManager, connection));
|
|
28
37
|
documents.listen(connection);
|