aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs/src/keymap-upgrade/parser.ts
blob: b496d1d5a80cc5e34d170474fb81d74fba0713ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import Parser from "web-tree-sitter";

const TREE_SITTER_WASM_URL = new URL(
  "/node_modules/web-tree-sitter/tree-sitter.wasm",
  import.meta.url
);

const TREE_SITTER_DEVICETREE_WASM_URL = new URL(
  "/node_modules/tree-sitter-devicetree/tree-sitter-devicetree.wasm",
  import.meta.url
);

export let Devicetree: Parser.Language;

export async function initParser() {
  await Parser.init({
    locateFile: (path: string, prefix: string) => {
      // When locating tree-sitter.wasm, use a path that Webpack can map to the correct URL.
      if (path == "tree-sitter.wasm") {
        return TREE_SITTER_WASM_URL.href;
      }
      return prefix + path;
    },
  });
  Devicetree = await Parser.Language.load(TREE_SITTER_DEVICETREE_WASM_URL.href);
}

export function createParser() {
  if (!Devicetree) {
    throw new Error("Parser not loaded. Call initParser() first.");
  }

  const parser = new Parser();
  parser.setLanguage(Devicetree);
  return parser;
}

/**
 * Returns the node for the named capture.
 */
export function findCapture(name: string, captures: Parser.QueryCapture[]) {
  for (const c of captures) {
    if (c.name === name) {
      return c.node;
    }
  }

  return null;
}

/**
 * Returns whether the node for the named capture exists and has the given text.
 */
export function captureHasText(
  name: string,
  captures: Parser.QueryCapture[],
  text: string
) {
  const node = findCapture(name, captures);
  return node?.text === text;
}

/**
 * Get a list of SyntaxNodes representing a devicetree node with the given path.
 * The same node may be listed multiple times within a file.
 *
 * This function does not evaluate which node a reference points to, so given
 * a file containing "/ { foo: bar {}; }; &foo {};" searching for "&foo" will
 * return the "&foo {}" node but not "foo: bar {}".
 *
 * @param path Path to the node to find. May be an absolute path such as
 * "/foo/bar", a node reference such as "&foo", or a node reference followed by
 * a relative path such as "&foo/bar".
 */
export function findDevicetreeNode(
  tree: Parser.Tree,
  path: string
): Parser.SyntaxNode[] {
  const query = Devicetree.query("(node) @node");
  const matches = query.matches(tree.rootNode);

  const result: Parser.SyntaxNode[] = [];

  for (const { captures } of matches) {
    const node = findCapture("node", captures);

    if (node && getDevicetreeNodePath(node) === path) {
      result.push(node);
    }
  }

  return result;
}

export interface FindPropertyOptions {
  /** Search in children of the given node as well */
  recursive?: boolean;
}

/**
 * Find all instances of a devicetree property with the given name which are
 * descendants of the given syntax node.
 *
 * @param node Any syntax node
 */
export function findDevicetreeProperty(
  node: Parser.SyntaxNode,
  name: string,
  options: FindPropertyOptions & { recursive: true }
): Parser.SyntaxNode[];

/**
 * Find a devicetree node's property with the given name, or null if it doesn't
 * have one.
 *
 * @note If the node contains multiple instances of the same property, this
 * returns the last once, since that is the one that will actually be applied.
 *
 * @param node A syntax node for a devicetree node
 */
export function findDevicetreeProperty(
  node: Parser.SyntaxNode,
  name: string,
  options?: FindPropertyOptions
): Parser.SyntaxNode | null;

export function findDevicetreeProperty(
  node: Parser.SyntaxNode,
  name: string,
  options?: FindPropertyOptions
): Parser.SyntaxNode[] | Parser.SyntaxNode | null {
  const query = Devicetree.query(
    `(property name: (identifier) @name (#eq? @name "${name}")) @prop`
  );
  const matches = query.matches(node);
  const props = matches.map(({ captures }) => findCapture("prop", captures)!);

  if (options?.recursive) {
    return props;
  }

  // The query finds all descendants. Filter to just the properties that belong
  // to the given devicetree node.
  const childProps = props.filter((prop) =>
    getContainingDevicetreeNode(prop)?.equals(node)
  );

  // Sort in descending order to select the last instance of the property.
  childProps.sort((a, b) => b.startIndex - a.startIndex);
  return childProps[0] ?? null;
}

export function getDevicetreeNodePath(node: Parser.SyntaxNode | null) {
  const parts = getDevicetreeNodePathParts(node);

  if (parts.length === 0) {
    return "";
  }

  if (parts.length === 1) {
    return parts[0];
  }

  const path = parts.join("/");

  // The top-level node should be named "/", which is a special case since the
  // path should not start with "//".
  return parts[0] === "/" ? path.substring(1) : path;
}

function getDevicetreeNodePathParts(node: Parser.SyntaxNode | null): string[] {
  // There may be intermediate syntax nodes between devicetree nodes, such as
  // #if blocks, so if we aren't currently on a "node" node, traverse up the
  // tree until we find one.
  const dtnode = getContainingDevicetreeNode(node);
  if (!dtnode) {
    return [];
  }

  const name = dtnode.childForFieldName("name")?.text ?? "";

  return [...getDevicetreeNodePathParts(dtnode.parent), name];
}

export function getContainingDevicetreeNode(node: Parser.SyntaxNode | null) {
  while (node && node.type !== "node") {
    node = node.parent;
  }
  return node;
}