ground-control-cli 0.2.0__py3-none-any.whl
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.
- client/__init__.py +1 -0
- client/claude.py +240 -0
- client/cli.py +314 -0
- client/connect.py +128 -0
- client/machine.py +254 -0
- client/probes/__init__.py +57 -0
- client/probes/code/__init__.py +435 -0
- client/probes/code/bash.py +163 -0
- client/probes/code/javascript.js +171 -0
- client/probes/code/javascript.py +193 -0
- client/probes/code/python.py +143 -0
- client/probes/errors.py +67 -0
- client/probes/gh.py +126 -0
- client/probes/git.py +280 -0
- client/probes/health.py +128 -0
- client/probes/settings.py +72 -0
- client/probes/tests.py +532 -0
- client/repos.py +68 -0
- client/service.py +398 -0
- client/system/__init__.py +73 -0
- client/system/macos.py +92 -0
- client/update.py +110 -0
- ground_control_cli-0.2.0.dist-info/METADATA +5 -0
- ground_control_cli-0.2.0.dist-info/RECORD +26 -0
- ground_control_cli-0.2.0.dist-info/WHEEL +4 -0
- ground_control_cli-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// The JavaScript and TypeScript reader's half that runs on Node, with the TypeScript compiler API.
|
|
2
|
+
//
|
|
3
|
+
// Reads on stdin {"typescript": <the compiler's package folder>, "name": <the file as shown>,
|
|
4
|
+
// "blocks": [{"text", "kind": js|jsx|ts|tsx, "offset": lines before the block}]} and writes on
|
|
5
|
+
// stdout {"symbols": [...], "calls": [...]} or {"error": "..."}. See javascript.py for what a
|
|
6
|
+
// symbol is.
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const input = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
10
|
+
const ts = require(input.typescript);
|
|
11
|
+
const K = ts.SyntaxKind;
|
|
12
|
+
const KINDS = { js: ts.ScriptKind.JS, jsx: ts.ScriptKind.JSX, ts: ts.ScriptKind.TS,
|
|
13
|
+
tsx: ts.ScriptKind.TSX };
|
|
14
|
+
const OPEN = "([{";
|
|
15
|
+
const CLOSE = ")]}";
|
|
16
|
+
const TRIVIA = new Set([K.WhitespaceTrivia, K.NewLineTrivia, K.SingleLineCommentTrivia,
|
|
17
|
+
K.MultiLineCommentTrivia, K.ShebangTrivia, K.ConflictMarkerTrivia]);
|
|
18
|
+
|
|
19
|
+
// The text from start to end on one line, comments dropped, joined the way the Python reader
|
|
20
|
+
// joins a header: one space where the source had a gap, none after an opener or before a closer.
|
|
21
|
+
function oneLine(sf, start, end) {
|
|
22
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, sf.languageVariant,
|
|
23
|
+
sf.text.slice(start, end));
|
|
24
|
+
const parts = [];
|
|
25
|
+
const templates = [];
|
|
26
|
+
let braces = 0;
|
|
27
|
+
let prevEnd = null;
|
|
28
|
+
for (let t = scanner.scan(); t !== K.EndOfFileToken; t = scanner.scan()) {
|
|
29
|
+
if (TRIVIA.has(t)) continue;
|
|
30
|
+
if (t === K.TemplateHead) templates.push(braces);
|
|
31
|
+
else if (t === K.OpenBraceToken) braces++;
|
|
32
|
+
else if (t === K.CloseBraceToken) {
|
|
33
|
+
if (templates.length && templates[templates.length - 1] === braces) {
|
|
34
|
+
t = scanner.reScanTemplateToken(false);
|
|
35
|
+
if (t === K.TemplateTail) templates.pop();
|
|
36
|
+
} else braces--;
|
|
37
|
+
}
|
|
38
|
+
const text = scanner.getTokenText();
|
|
39
|
+
const at = scanner.getTokenPos ? scanner.getTokenPos() : scanner.getTokenStart();
|
|
40
|
+
const gap = prevEnd !== null && at !== prevEnd;
|
|
41
|
+
if (parts.length && gap && !OPEN.includes(parts[parts.length - 1]) && !CLOSE.includes(text)) {
|
|
42
|
+
parts.push(" ");
|
|
43
|
+
}
|
|
44
|
+
parts.push(text);
|
|
45
|
+
prevEnd = scanner.getTextPos();
|
|
46
|
+
}
|
|
47
|
+
return parts.join("").trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function child(node, sf, kinds) {
|
|
51
|
+
return node.getChildren(sf).find((c) => kinds.includes(c.kind));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nameOf(node, sf) {
|
|
55
|
+
if (!node.name) return "default";
|
|
56
|
+
return ts.isIdentifier(node.name) || ts.isPrivateIdentifier(node.name)
|
|
57
|
+
? node.name.text : node.name.getText(sf);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// From the type parameters or the parameters of a function to its body (or its `=>`).
|
|
61
|
+
function params(fn, sf) {
|
|
62
|
+
const open = child(fn, sf, [K.LessThanToken, K.OpenParenToken]);
|
|
63
|
+
const start = open ? open.getStart(sf) : fn.parameters[0].getStart(sf);
|
|
64
|
+
const end = ts.isArrowFunction(fn) ? fn.equalsGreaterThanToken.getStart(sf)
|
|
65
|
+
: fn.body.getStart(sf);
|
|
66
|
+
return oneLine(sf, start, end);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isFunction(node) {
|
|
70
|
+
return node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unwrap(node) {
|
|
74
|
+
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) ||
|
|
75
|
+
ts.isSatisfiesExpression && ts.isSatisfiesExpression(node))) {
|
|
76
|
+
node = node.expression;
|
|
77
|
+
}
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readBlock(block, found) {
|
|
82
|
+
const sf = ts.createSourceFile(input.name, block.text, ts.ScriptTarget.Latest, true,
|
|
83
|
+
KINDS[block.kind]);
|
|
84
|
+
const line = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1 + block.offset;
|
|
85
|
+
const problems = sf.parseDiagnostics || [];
|
|
86
|
+
if (problems.length) {
|
|
87
|
+
const p = problems[0];
|
|
88
|
+
const message = ts.flattenDiagnosticMessageText(p.messageText, " ");
|
|
89
|
+
throw new Error(`${input.name}: TypeScript cannot parse it: line ${line(p.start)}: ${message}`);
|
|
90
|
+
}
|
|
91
|
+
const symbols = [];
|
|
92
|
+
const add = (kind, scope, signature, span) => {
|
|
93
|
+
symbols.push({ kind, name: scope.join("."), signature, first: line(span.getStart(sf)),
|
|
94
|
+
last: line(span.getEnd()) });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function members(cls, scope) {
|
|
98
|
+
for (const m of cls.members) {
|
|
99
|
+
if ((ts.isMethodDeclaration(m) || ts.isConstructorDeclaration(m) ||
|
|
100
|
+
ts.isGetAccessorDeclaration(m) || ts.isSetAccessorDeclaration(m)) && m.body) {
|
|
101
|
+
const name = ts.isConstructorDeclaration(m) ? "constructor" : nameOf(m, sf);
|
|
102
|
+
const from = ts.isGetAccessorDeclaration(m) || ts.isSetAccessorDeclaration(m)
|
|
103
|
+
? child(m, sf, [K.GetKeyword, K.SetKeyword])
|
|
104
|
+
: ts.isConstructorDeclaration(m) ? child(m, sf, [K.ConstructorKeyword]) : m.name;
|
|
105
|
+
add("method", [...scope, name], oneLine(sf, from.getStart(sf), m.body.getStart(sf)), m);
|
|
106
|
+
} else if (ts.isPropertyDeclaration(m) && isFunction(unwrap(m.initializer))) {
|
|
107
|
+
const name = nameOf(m, sf);
|
|
108
|
+
add("method", [...scope, name], name + params(unwrap(m.initializer), sf), m);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cls(node, name, scope, span) {
|
|
114
|
+
const brace = child(node, sf, [K.OpenBraceToken]);
|
|
115
|
+
const from = node.name ? node.name.getStart(sf) : brace.getStart(sf);
|
|
116
|
+
const header = node.name ? oneLine(sf, from, brace.getStart(sf)) : name;
|
|
117
|
+
add("class", [...scope, name], header, span);
|
|
118
|
+
members(node, [...scope, name]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const st of sf.statements) {
|
|
122
|
+
if (ts.isFunctionDeclaration(st) && st.body) {
|
|
123
|
+
const name = nameOf(st, sf);
|
|
124
|
+
add("function", [name], name + params(st, sf), st);
|
|
125
|
+
} else if (ts.isClassDeclaration(st)) {
|
|
126
|
+
cls(st, nameOf(st, sf), [], st);
|
|
127
|
+
} else if (ts.isVariableStatement(st)) {
|
|
128
|
+
const list = st.declarationList.declarations;
|
|
129
|
+
for (const d of list) {
|
|
130
|
+
const value = unwrap(d.initializer);
|
|
131
|
+
if (!ts.isIdentifier(d.name) || !value) continue;
|
|
132
|
+
const span = list.length === 1 ? st : d;
|
|
133
|
+
if (isFunction(value)) add("function", [d.name.text], d.name.text + params(value, sf), span);
|
|
134
|
+
else if (ts.isClassExpression(value)) cls(value, d.name.text, [], span);
|
|
135
|
+
}
|
|
136
|
+
} else if (ts.isExportAssignment(st) && isFunction(unwrap(st.expression))) {
|
|
137
|
+
add("function", ["default"], "default" + params(unwrap(st.expression), sf), st);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const calls = [];
|
|
142
|
+
const visit = (node) => {
|
|
143
|
+
let callee = null;
|
|
144
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) callee = node.expression;
|
|
145
|
+
else if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
|
|
146
|
+
!/^[a-z]/.test(node.tagName.getText(sf))) callee = node.tagName; // not <div>
|
|
147
|
+
if (callee) {
|
|
148
|
+
const name = ts.isIdentifier(callee) ? callee.text
|
|
149
|
+
: ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
150
|
+
if (name) calls.push({ callee: name, line: line(node.getStart(sf)) });
|
|
151
|
+
}
|
|
152
|
+
ts.forEachChild(node, visit);
|
|
153
|
+
};
|
|
154
|
+
visit(sf);
|
|
155
|
+
|
|
156
|
+
found.symbols.push(...symbols);
|
|
157
|
+
for (const c of calls) {
|
|
158
|
+
const inside = symbols.filter((s) => s.first <= c.line && c.line <= s.last);
|
|
159
|
+
inside.sort((a, b) => (a.last - a.first) - (b.last - b.first));
|
|
160
|
+
found.calls.push({ caller: inside.length ? inside[0].name : "<module>", ...c });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const found = { symbols: [], calls: [] };
|
|
166
|
+
for (const block of input.blocks) readBlock(block, found);
|
|
167
|
+
found.calls.sort((a, b) => a.line - b.line);
|
|
168
|
+
process.stdout.write(JSON.stringify(found));
|
|
169
|
+
} catch (e) {
|
|
170
|
+
process.stdout.write(JSON.stringify({ error: e.message }));
|
|
171
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""The JavaScript and TypeScript reader, with the TypeScript compiler API on Node.
|
|
2
|
+
|
|
3
|
+
It reads .js .mjs .cjs .jsx .ts .tsx, and the inline `<script>` blocks of .html files (those
|
|
4
|
+
without `src` whose type is JavaScript), each block parsed on its own with the file's lines.
|
|
5
|
+
Symbols are the module's function declarations, its `const`/`let`/`var` bound to a function or
|
|
6
|
+
an arrow function, its classes (declared or bound to a variable), and the methods of its classes:
|
|
7
|
+
methods, the constructor, get and set accessors, and properties bound to a function. A function
|
|
8
|
+
inside a function is part of the outer one, as in the Python reader. `export default` without a
|
|
9
|
+
name is named `default`.
|
|
10
|
+
|
|
11
|
+
The signature is the header as written, from the name to the body (to the `=>` for an arrow),
|
|
12
|
+
on one line, comments dropped; a function bound to a variable is its variable's name followed
|
|
13
|
+
by its parameters. The first line is the statement's, decorators and modifiers included.
|
|
14
|
+
|
|
15
|
+
The compiler is TypeScript 6.0.3, the method's own private copy: installed with
|
|
16
|
+
`npm install --prefix <the method's data folder> typescript@6.0.3` and loaded from there, never
|
|
17
|
+
from a global install. The data folder is METHOD_DATA if set, else ~/.method. `missing()`
|
|
18
|
+
says what to install when Node or that copy is not there.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
from html.parser import HTMLParser
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from .. import errors, settings
|
|
30
|
+
|
|
31
|
+
KINDS = {".js": "js", ".mjs": "js", ".cjs": "js", ".jsx": "jsx", ".ts": "ts", ".tsx": "tsx"}
|
|
32
|
+
HTML = ".html"
|
|
33
|
+
SCRIPT_TYPES = ("", "module", "text/javascript", "application/javascript")
|
|
34
|
+
HELPER = Path(__file__).with_name("javascript.js")
|
|
35
|
+
COMPILER = "typescript@6.0.3"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def data_dir() -> Path:
|
|
39
|
+
"""The method's own data folder: METHOD_DATA if set, else ~/.method."""
|
|
40
|
+
return settings.method_data()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compiler() -> Path:
|
|
44
|
+
"""Where the method's copy of the TypeScript compiler is (or is to be) installed."""
|
|
45
|
+
return data_dir() / "node_modules" / "typescript"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def install_line() -> str:
|
|
49
|
+
"""Return the npm line that installs the method's copy of the TypeScript compiler.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
An `npm install --prefix` line into the method's data folder.
|
|
53
|
+
|
|
54
|
+
Company: assumes none.
|
|
55
|
+
"""
|
|
56
|
+
return f"npm install --prefix {data_dir()} {COMPILER}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _installed() -> bool:
|
|
60
|
+
try:
|
|
61
|
+
found = json.loads((compiler() / "package.json").read_text(encoding="utf-8"))
|
|
62
|
+
except (OSError, ValueError):
|
|
63
|
+
return False
|
|
64
|
+
return f"typescript@{found.get('version')}" == COMPILER
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def missing() -> str:
|
|
68
|
+
"""What to install for this reader to run, or "" when Node and the compiler are there."""
|
|
69
|
+
if not shutil.which("node"):
|
|
70
|
+
return (
|
|
71
|
+
f"install Node.js (https://nodejs.org), then the TypeScript compiler: {install_line()}"
|
|
72
|
+
)
|
|
73
|
+
if not _installed():
|
|
74
|
+
return f"install the TypeScript compiler: {install_line()}"
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def tools_missing() -> list:
|
|
79
|
+
"""The names of its missing tools: node, the method's typescript, or none."""
|
|
80
|
+
if not shutil.which("node"):
|
|
81
|
+
return ["node", "typescript"] if not _installed() else ["node"]
|
|
82
|
+
return [] if _installed() else ["typescript"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _Scripts(HTMLParser):
|
|
86
|
+
"""The inline JavaScript blocks of an HTML page: (lines before the block, its text)."""
|
|
87
|
+
|
|
88
|
+
def __init__(self):
|
|
89
|
+
super().__init__(convert_charrefs=False)
|
|
90
|
+
self.blocks, self._open = [], None
|
|
91
|
+
|
|
92
|
+
def handle_starttag(self, tag, attrs):
|
|
93
|
+
if tag != "script":
|
|
94
|
+
return
|
|
95
|
+
found = {k.lower(): (v or "") for k, v in attrs}
|
|
96
|
+
inline = "src" not in found and found.get("type", "").strip().lower() in SCRIPT_TYPES
|
|
97
|
+
self._open = [None, []] if inline else None
|
|
98
|
+
|
|
99
|
+
def handle_data(self, data):
|
|
100
|
+
if self._open is not None:
|
|
101
|
+
if self._open[0] is None:
|
|
102
|
+
self._open[0] = self.getpos()[0] - 1
|
|
103
|
+
self._open[1].append(data)
|
|
104
|
+
|
|
105
|
+
def handle_endtag(self, tag):
|
|
106
|
+
if tag == "script" and self._open is not None:
|
|
107
|
+
offset, parts = self._open
|
|
108
|
+
if "".join(parts).strip():
|
|
109
|
+
self.blocks.append((offset, "".join(parts)))
|
|
110
|
+
self._open = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _blocks(path: Path) -> list:
|
|
114
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
115
|
+
if path.suffix.lower() != HTML:
|
|
116
|
+
return [{"text": text, "kind": KINDS[path.suffix.lower()], "offset": 0}]
|
|
117
|
+
scripts = _Scripts()
|
|
118
|
+
scripts.feed(text)
|
|
119
|
+
scripts.close()
|
|
120
|
+
return [{"text": t, "kind": "js", "offset": offset} for offset, t in scripts.blocks]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _read(path: Path, name: str) -> dict:
|
|
124
|
+
need = missing()
|
|
125
|
+
if need:
|
|
126
|
+
raise errors.Refused(
|
|
127
|
+
f"{name}: reading it needs Node and the TypeScript compiler: {need}",
|
|
128
|
+
errors.TOOL_MISSING,
|
|
129
|
+
)
|
|
130
|
+
blocks = _blocks(path)
|
|
131
|
+
if not blocks:
|
|
132
|
+
return {"symbols": [], "calls": []}
|
|
133
|
+
request = {"typescript": str(compiler()), "name": name, "blocks": blocks}
|
|
134
|
+
out = subprocess.run(
|
|
135
|
+
[shutil.which("node"), str(HELPER)],
|
|
136
|
+
input=json.dumps(request),
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
found = json.loads(out.stdout)
|
|
142
|
+
except ValueError as e:
|
|
143
|
+
raise errors.Refused(
|
|
144
|
+
f"{name}: the TypeScript reader failed: {(out.stderr or out.stdout).strip()[-500:]}"
|
|
145
|
+
) from e
|
|
146
|
+
if "error" in found:
|
|
147
|
+
raise errors.Refused(found["error"])
|
|
148
|
+
return found
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def symbols(path: Path, name: str) -> list:
|
|
152
|
+
"""Return the file's functions, classes and methods, as the Node helper reads them.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
path: The file to read.
|
|
156
|
+
name: The file as it is shown.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A Symbol for each; none for an HTML file without inline scripts.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
errors.Refused: If Node or the compiler is missing, or the helper fails or reports an
|
|
163
|
+
error.
|
|
164
|
+
|
|
165
|
+
Company: assumes none.
|
|
166
|
+
"""
|
|
167
|
+
from . import Symbol
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
Symbol(s["kind"], s["name"], name, s["signature"], s["first"], s["last"])
|
|
171
|
+
for s in _read(path, name)["symbols"]
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def calls(path: Path, name: str) -> list:
|
|
176
|
+
"""Return each call in the file, `new` and JSX elements included.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: The file to read.
|
|
180
|
+
name: The file as it is shown.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A Call for each: the enclosing symbol (or `<module>`), the called name, the line.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
errors.Refused: If Node or the compiler is missing, or the helper fails or reports an
|
|
187
|
+
error.
|
|
188
|
+
|
|
189
|
+
Company: assumes none.
|
|
190
|
+
"""
|
|
191
|
+
from . import Call
|
|
192
|
+
|
|
193
|
+
return [Call(c["caller"], c["callee"], name, c["line"]) for c in _read(path, name)["calls"]]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""The Python reader, with `ast`.
|
|
2
|
+
|
|
3
|
+
Symbols are the module's classes and functions, and the methods of its classes (nested classes
|
|
4
|
+
included). A function inside a function is part of the outer one, not a symbol of its own.
|
|
5
|
+
The qualified name joins the enclosing classes with `.`; the signature is the header as
|
|
6
|
+
written, from the name to the colon, on one line, comments dropped. The first line is the
|
|
7
|
+
first decorator's.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import io
|
|
14
|
+
import tokenize
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .. import errors
|
|
18
|
+
|
|
19
|
+
DEFS = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
20
|
+
OPEN, CLOSE = "([{", ")]}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def missing() -> str:
|
|
24
|
+
"""Nothing to install: `ast` is part of Python."""
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tools_missing() -> list:
|
|
29
|
+
"""None of its tools is missing: `ast` is part of Python."""
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _tree(path: Path, name: str):
|
|
34
|
+
text = path.read_text(encoding="utf-8")
|
|
35
|
+
try:
|
|
36
|
+
return text, ast.parse(text, filename=name)
|
|
37
|
+
except SyntaxError as e:
|
|
38
|
+
raise errors.Refused(f"{name}: Python cannot parse it: line {e.lineno}: {e.msg}") from e
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _header(lines: list, node) -> str:
|
|
42
|
+
"""From the name after `def`/`class` up to the colon that ends the header."""
|
|
43
|
+
first = node.lineno - 1
|
|
44
|
+
source = "".join(lines[first : node.body[0].lineno])
|
|
45
|
+
tokens = tokenize.generate_tokens(io.StringIO(source).readline)
|
|
46
|
+
parts, depth, prev_end, seen_keyword = [], 0, None, False
|
|
47
|
+
for tok in tokens:
|
|
48
|
+
if tok.type in (
|
|
49
|
+
tokenize.COMMENT,
|
|
50
|
+
tokenize.NL,
|
|
51
|
+
tokenize.NEWLINE,
|
|
52
|
+
tokenize.INDENT,
|
|
53
|
+
tokenize.DEDENT,
|
|
54
|
+
):
|
|
55
|
+
continue
|
|
56
|
+
if not seen_keyword:
|
|
57
|
+
if tok.string in ("def", "class"):
|
|
58
|
+
seen_keyword = True
|
|
59
|
+
prev_end = tok.end
|
|
60
|
+
continue
|
|
61
|
+
if tok.string == ":" and depth == 0:
|
|
62
|
+
break
|
|
63
|
+
if tok.string and tok.string in OPEN:
|
|
64
|
+
depth += 1
|
|
65
|
+
elif tok.string and tok.string in CLOSE:
|
|
66
|
+
depth -= 1
|
|
67
|
+
gap = prev_end is not None and tok.start != prev_end
|
|
68
|
+
if parts and gap and parts[-1] not in OPEN and tok.string not in CLOSE:
|
|
69
|
+
parts.append(" ")
|
|
70
|
+
parts.append(tok.string)
|
|
71
|
+
prev_end = tok.end
|
|
72
|
+
return "".join(parts).strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _walk(body, scope: list, inside_class: bool):
|
|
76
|
+
for node in body:
|
|
77
|
+
if isinstance(node, ast.ClassDef):
|
|
78
|
+
yield node, "class", scope + [node.name]
|
|
79
|
+
yield from _walk(node.body, scope + [node.name], True)
|
|
80
|
+
elif isinstance(node, DEFS):
|
|
81
|
+
yield node, "method" if inside_class else "function", scope + [node.name]
|
|
82
|
+
elif isinstance(node, (ast.If, ast.Try, ast.With, ast.AsyncWith)):
|
|
83
|
+
# definitions under `if TYPE_CHECKING:`, `try: ... except ImportError:` and the like
|
|
84
|
+
for field in ("body", "orelse", "finalbody"):
|
|
85
|
+
yield from _walk(getattr(node, field, []), scope, inside_class)
|
|
86
|
+
for handler in getattr(node, "handlers", []):
|
|
87
|
+
yield from _walk(handler.body, scope, inside_class)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _first(node) -> int:
|
|
91
|
+
"""The first line of the symbol: its first decorator, or its `def`/`class` line."""
|
|
92
|
+
return min([node.lineno] + [d.lineno for d in node.decorator_list])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def symbols(path: Path, name: str) -> list:
|
|
96
|
+
"""Return the module's classes and functions, and the methods of its classes.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
path: The file to read.
|
|
100
|
+
name: The file as it is shown.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A Symbol for each, in the order they are written.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
errors.Refused: If Python cannot parse the file.
|
|
107
|
+
|
|
108
|
+
Company: assumes none.
|
|
109
|
+
"""
|
|
110
|
+
from . import Symbol
|
|
111
|
+
|
|
112
|
+
text, tree = _tree(path, name)
|
|
113
|
+
lines = text.splitlines(keepends=True)
|
|
114
|
+
return [
|
|
115
|
+
Symbol(kind, ".".join(qualified), name, _header(lines, node), _first(node), node.end_lineno)
|
|
116
|
+
for node, kind, qualified in _walk(tree.body, [], False)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def calls(path: Path, name: str) -> list:
|
|
121
|
+
"""Each call in the file: the enclosing symbol (or `<module>`), the called name, the line."""
|
|
122
|
+
from . import Call
|
|
123
|
+
|
|
124
|
+
_, tree = _tree(path, name)
|
|
125
|
+
spans = [
|
|
126
|
+
(_first(node), node.end_lineno, ".".join(q)) for node, _, q in _walk(tree.body, [], False)
|
|
127
|
+
]
|
|
128
|
+
found = []
|
|
129
|
+
for node in ast.walk(tree):
|
|
130
|
+
if not isinstance(node, ast.Call):
|
|
131
|
+
continue
|
|
132
|
+
func = node.func
|
|
133
|
+
callee = (
|
|
134
|
+
func.id
|
|
135
|
+
if isinstance(func, ast.Name)
|
|
136
|
+
else (func.attr if isinstance(func, ast.Attribute) else "")
|
|
137
|
+
)
|
|
138
|
+
if not callee:
|
|
139
|
+
continue
|
|
140
|
+
inside = [s for s in spans if s[0] <= node.lineno <= s[1]]
|
|
141
|
+
caller = min(inside, key=lambda s: s[1] - s[0])[2] if inside else "<module>"
|
|
142
|
+
found.append(Call(caller, callee, name, node.lineno))
|
|
143
|
+
return sorted(found, key=lambda c: c.line)
|
client/probes/errors.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Why a probe could not find its fact, as the service's facts name it (app/method/facts.py).
|
|
2
|
+
|
|
3
|
+
A probe says which of these stopped it, never what a command printed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
NO_COMMAND = "no_command" # the repo names no command for this
|
|
9
|
+
COMMAND_FAILED = "command_failed" # the repo's command ran and failed
|
|
10
|
+
NO_READER = "no_reader" # no code reader reads the file
|
|
11
|
+
TOOL_MISSING = "tool_missing" # a reader's or git's own tooling is missing
|
|
12
|
+
UNREADABLE = "unreadable" # the reader could not parse the file
|
|
13
|
+
NOT_FOUND = "not_found" # the ref, file, PR or address is not there
|
|
14
|
+
TIMED_OUT = "timed_out" # it did not finish in the repo's time
|
|
15
|
+
REFUSED = "refused" # git or gh refused
|
|
16
|
+
|
|
17
|
+
ERRORS = (
|
|
18
|
+
NO_COMMAND,
|
|
19
|
+
COMMAND_FAILED,
|
|
20
|
+
NO_READER,
|
|
21
|
+
TOOL_MISSING,
|
|
22
|
+
UNREADABLE,
|
|
23
|
+
NOT_FOUND,
|
|
24
|
+
TIMED_OUT,
|
|
25
|
+
REFUSED,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# What git and gh print when the ref, the commit or the PR is not there.
|
|
29
|
+
NOT_THERE = (
|
|
30
|
+
"unknown revision",
|
|
31
|
+
"bad revision",
|
|
32
|
+
"bad object",
|
|
33
|
+
"not a valid object name",
|
|
34
|
+
"not a valid commit name",
|
|
35
|
+
"could not resolve",
|
|
36
|
+
"no pull requests found",
|
|
37
|
+
"not found",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Refused(Exception):
|
|
42
|
+
"""A code reader could not read a file; `error` is which of ERRORS stopped it.
|
|
43
|
+
|
|
44
|
+
The message says why, for the person on this machine; a probe returns only the error.
|
|
45
|
+
|
|
46
|
+
Company: assumes none.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, error: str = UNREADABLE):
|
|
50
|
+
"""Keep the message and the error."""
|
|
51
|
+
super().__init__(message)
|
|
52
|
+
self.error = error
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def failed(stderr: str) -> str:
|
|
56
|
+
"""The error a failed git or gh command stands for: not_found or refused.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
stderr: What the command printed on standard error, read here and never kept.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
NOT_FOUND when it says the thing asked for is not there; REFUSED otherwise.
|
|
63
|
+
|
|
64
|
+
Company: assumes none.
|
|
65
|
+
"""
|
|
66
|
+
said = (stderr or "").lower()
|
|
67
|
+
return NOT_FOUND if any(n in said for n in NOT_THERE) else REFUSED
|