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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/readme.md +11 -0
  3. package/src/handlers/diagnostics/action.d.ts +6 -0
  4. package/src/handlers/diagnostics/action.js +14 -0
  5. package/src/handlers/diagnostics/base.d.ts +9 -0
  6. package/src/handlers/diagnostics/base.js +21 -0
  7. package/src/handlers/diagnostics/condition.d.ts +6 -0
  8. package/src/handlers/diagnostics/condition.js +7 -0
  9. package/src/handlers/diagnostics/diagnostics.d.ts +4 -0
  10. package/src/handlers/diagnostics/diagnostics.js +121 -0
  11. package/src/handlers/diagnostics/effect.d.ts +6 -0
  12. package/src/handlers/diagnostics/effect.js +12 -0
  13. package/src/handlers/diagnostics/principal.d.ts +6 -0
  14. package/src/handlers/diagnostics/principal.js +7 -0
  15. package/src/handlers/diagnostics/resource.d.ts +6 -0
  16. package/src/handlers/diagnostics/resource.js +7 -0
  17. package/src/handlers/diagnostics/sid.d.ts +8 -0
  18. package/src/handlers/diagnostics/sid.js +21 -0
  19. package/src/handlers/diagnostics/utils.d.ts +3 -0
  20. package/src/handlers/diagnostics/utils.js +19 -0
  21. package/src/handlers/document-link/document-link.d.ts +1 -1
  22. package/src/handlers/document-link/document-link.js +37 -27
  23. package/src/lib/iam-policy/reference/services.d.ts +1 -1
  24. package/src/lib/iam-policy/reference/services.js +4 -2
  25. package/src/lib/treesitter/base.d.ts +28 -2
  26. package/src/lib/treesitter/base.js +9 -0
  27. package/src/lib/treesitter/hcl.d.ts +2 -1
  28. package/src/lib/treesitter/hcl.js +262 -1
  29. package/src/lib/treesitter/json.d.ts +2 -1
  30. package/src/lib/treesitter/json.js +126 -1
  31. package/src/lib/treesitter/yaml.d.ts +2 -1
  32. package/src/lib/treesitter/yaml.js +135 -1
  33. package/src/server.js +12 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-iam-language-server",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "bin": "./src/server.js",
6
6
  "publisher": "MichaelBarney",
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,7 @@
1
+ import { ElementValidator } from "./base.js";
2
+ export class ConditionValidator extends ElementValidator {
3
+ validate(entry) {
4
+ const diagnostics = super.validate(entry);
5
+ return diagnostics;
6
+ }
7
+ }
@@ -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,7 @@
1
+ import { ElementValidator } from "./base.js";
2
+ export class PrincipalValidator extends ElementValidator {
3
+ validate(entry) {
4
+ const diagnostics = super.validate(entry);
5
+ return diagnostics;
6
+ }
7
+ }
@@ -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,7 @@
1
+ import { ElementValidator } from "./base.js";
2
+ export class ResourceValidator extends ElementValidator {
3
+ validate(entry) {
4
+ const diagnostics = super.validate(entry);
5
+ return diagnostics;
6
+ }
7
+ }
@@ -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,3 @@
1
+ import type { Diagnostic } from 'vscode-languageclient';
2
+ import type { Range } from '../../lib/treesitter/base.ts';
3
+ export declare function createDiagnostic(message: string, range: Range): Diagnostic;
@@ -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>, _treeManager: TreeManager, connection: Connection): DocumentLink[];
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, _treeManager, connection) {
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 text = document.getText();
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 actionSet = new Set(ServiceReference.getAllActions().map((a) => a.toLowerCase()));
9
- const pattern = /[a-z0-9-]+:[A-Za-z0-9*?]+/g;
10
- for (const match of text.matchAll(pattern)) {
11
- if (!actionSet.has(match[0].toLowerCase()))
12
- continue;
13
- const [serviceName, actionName] = match[0].split(':');
14
- const action = ServiceReference.getAction(serviceName, actionName);
15
- if (!action)
16
- continue;
17
- const index = match.index ?? 0;
18
- const start = document.positionAt(index);
19
- const end = document.positionAt(index + match[0].length);
20
- if (action.iamUrl) {
21
- links.push({
22
- range: { start, end },
23
- target: action.iamUrl,
24
- tooltip: 'IAM Actions, Conditions, and Context Keys',
25
- });
26
- }
27
- if (action.operationUrl) {
28
- links.push({
29
- range: { start, end },
30
- target: action.operationUrl,
31
- tooltip: 'API Operation Documentation',
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(service: string, actionName: string): Action | undefined;
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(service, actionName) {
63
+ static getAction(action) {
64
64
  try {
65
- return ServiceReference.getServiceData(service).actions[actionName];
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): import("web-tree-sitter").Node | null;
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 }) => treeManager.openDocument(document.uri, document.getText(), document.languageId));
24
- documents.onDidChangeContent(({ document }) => treeManager.updateDocument(document.uri, document.getText()));
25
- documents.onDidClose(({ document }) => treeManager.closeDocument(document.uri));
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);