@clue-ai/cli 0.0.9 → 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.
@@ -1,134 +1,197 @@
1
1
  const LIFECYCLE_CALL_PATTERN =
2
- /\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout)\s*\(/g;
3
- const SAFE_HELPER_PATTERN =
4
- /\b(?:safeClue|safe_clue|safe_clue_call|safeClueCall|withClueGuard|with_clue_guard)\s*\(/g;
2
+ /\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout|clue_init_fastapi|clue_init_django)\s*\(/g;
5
3
 
6
- const findMatchingDelimiter = (text, openIndex, open, close) => {
7
- let depth = 0;
8
- for (let index = openIndex; index < text.length; index += 1) {
9
- const character = text[index];
10
- if (character === open) depth += 1;
11
- if (character === close) {
12
- depth -= 1;
13
- if (depth === 0) return index;
4
+ export const stripSourceNoise = (text, { stripStrings = false } = {}) => {
5
+ let output = "";
6
+ let index = 0;
7
+ while (index < text.length) {
8
+ const char = text[index];
9
+ const next = text[index + 1];
10
+ if (char === "/" && next === "/") {
11
+ while (index < text.length && text[index] !== "\n") index += 1;
12
+ continue;
13
+ }
14
+ if (char === "#") {
15
+ while (index < text.length && text[index] !== "\n") index += 1;
16
+ continue;
14
17
  }
18
+ if (char === "/" && next === "*") {
19
+ index += 2;
20
+ while (
21
+ index < text.length &&
22
+ !(text[index] === "*" && text[index + 1] === "/")
23
+ ) {
24
+ if (text[index] === "\n") output += "\n";
25
+ index += 1;
26
+ }
27
+ index += index < text.length ? 2 : 0;
28
+ continue;
29
+ }
30
+ if (char === "'" || char === '"' || char === "`") {
31
+ const quote = char;
32
+ const triple =
33
+ quote !== "`" && text.slice(index, index + 3) === quote.repeat(3);
34
+ const endToken = triple ? quote.repeat(3) : quote;
35
+ if (!stripStrings) {
36
+ output += triple ? endToken : quote;
37
+ } else {
38
+ output += quote === "`" ? "``" : `${quote}${quote}`;
39
+ }
40
+ index += triple ? 3 : 1;
41
+ while (index < text.length) {
42
+ if (!triple && text[index] === "\\") {
43
+ if (!stripStrings) output += text.slice(index, index + 2);
44
+ index += 2;
45
+ continue;
46
+ }
47
+ if (text.slice(index, index + endToken.length) === endToken) {
48
+ if (!stripStrings) output += endToken;
49
+ index += endToken.length;
50
+ break;
51
+ }
52
+ if (text[index] === "\n") output += "\n";
53
+ if (!stripStrings && text[index] !== "\n") output += text[index];
54
+ index += 1;
55
+ }
56
+ continue;
57
+ }
58
+ output += char;
59
+ index += 1;
15
60
  }
16
- return -1;
61
+ return output;
17
62
  };
18
63
 
19
- const lineNumberForIndex = (text, index) =>
20
- text.slice(0, index).split("\n").length;
64
+ const isIdentifierCharacter = (character) =>
65
+ typeof character === "string" && /[A-Za-z0-9_$]/.test(character);
21
66
 
22
- const isAwaitedOnCallLine = (text, callIndex) => {
23
- const lineStart = text.lastIndexOf("\n", callIndex - 1) + 1;
24
- return /\bawait\b/.test(text.slice(lineStart, callIndex));
25
- };
67
+ const startsKeyword = (text, index, keyword) =>
68
+ text.startsWith(keyword, index) &&
69
+ !isIdentifierCharacter(text[index - 1]) &&
70
+ !isIdentifierCharacter(text[index + keyword.length]);
26
71
 
27
- const isInsideSafeHelperCall = (text, callIndex) => {
28
- for (const match of text.matchAll(SAFE_HELPER_PATTERN)) {
29
- const helperIndex = match.index ?? 0;
30
- if (helperIndex > callIndex) return false;
31
- const openParenIndex = text.indexOf("(", helperIndex);
32
- const closeParenIndex = findMatchingDelimiter(
33
- text,
34
- openParenIndex,
35
- "(",
36
- ")",
37
- );
38
- if (openParenIndex < callIndex && callIndex < closeParenIndex) return true;
72
+ const advanceQuotedString = (text, startIndex, { preserve = false } = {}) => {
73
+ const quote = text[startIndex];
74
+ const triple = quote !== "`" && text.slice(startIndex, startIndex + 3) === quote.repeat(3);
75
+ const endToken = triple ? quote.repeat(3) : quote;
76
+ let index = startIndex + (triple ? 3 : 1);
77
+ let value = preserve ? (triple ? endToken : quote) : "";
78
+ while (index < text.length) {
79
+ if (!triple && text[index] === "\\") {
80
+ if (preserve) value += text.slice(index, index + 2);
81
+ index += 2;
82
+ continue;
83
+ }
84
+ if (text.slice(index, index + endToken.length) === endToken) {
85
+ if (preserve) value += endToken;
86
+ index += endToken.length;
87
+ break;
88
+ }
89
+ if (preserve) value += text[index];
90
+ index += 1;
39
91
  }
40
- return false;
92
+ return { index, value };
41
93
  };
42
94
 
43
- const isInsideJsTryBlock = (text, callIndex) => {
44
- for (const match of text.matchAll(/\btry\s*{/g)) {
45
- const tryIndex = match.index ?? 0;
46
- if (tryIndex > callIndex) return false;
47
- const openBraceIndex = text.indexOf("{", tryIndex);
48
- const closeBraceIndex = findMatchingDelimiter(
49
- text,
50
- openBraceIndex,
51
- "{",
52
- "}",
53
- );
54
- if (openBraceIndex < callIndex && callIndex < closeBraceIndex) return true;
95
+ export const extractExecutableModuleStatements = (text) => {
96
+ const statements = [];
97
+ let index = 0;
98
+ while (index < text.length) {
99
+ const char = text[index];
100
+ const next = text[index + 1];
101
+ if (char === "/" && next === "/") {
102
+ while (index < text.length && text[index] !== "\n") index += 1;
103
+ continue;
104
+ }
105
+ if (char === "#") {
106
+ while (index < text.length && text[index] !== "\n") index += 1;
107
+ continue;
108
+ }
109
+ if (char === "/" && next === "*") {
110
+ index += 2;
111
+ while (
112
+ index < text.length &&
113
+ !(text[index] === "*" && text[index + 1] === "/")
114
+ ) {
115
+ index += 1;
116
+ }
117
+ index += index < text.length ? 2 : 0;
118
+ continue;
119
+ }
120
+ if (char === "'" || char === '"' || char === "`") {
121
+ index = advanceQuotedString(text, index).index;
122
+ continue;
123
+ }
124
+ if (!startsKeyword(text, index, "import") && !startsKeyword(text, index, "export")) {
125
+ index += 1;
126
+ continue;
127
+ }
128
+ let statement = "";
129
+ let depth = 0;
130
+ while (index < text.length) {
131
+ const current = text[index];
132
+ const following = text[index + 1];
133
+ if (current === "/" && following === "/") {
134
+ while (index < text.length && text[index] !== "\n") index += 1;
135
+ continue;
136
+ }
137
+ if (current === "/" && following === "*") {
138
+ index += 2;
139
+ while (
140
+ index < text.length &&
141
+ !(text[index] === "*" && text[index + 1] === "/")
142
+ ) {
143
+ index += 1;
144
+ }
145
+ index += index < text.length ? 2 : 0;
146
+ continue;
147
+ }
148
+ if (current === "'" || current === '"' || current === "`") {
149
+ const quoted = advanceQuotedString(text, index, { preserve: true });
150
+ statement += quoted.value;
151
+ index = quoted.index;
152
+ continue;
153
+ }
154
+ statement += current;
155
+ if (current === "{" || current === "(" || current === "[") depth += 1;
156
+ if (current === "}" || current === ")" || current === "]") {
157
+ depth = Math.max(0, depth - 1);
158
+ }
159
+ index += 1;
160
+ if (current === ";" && depth === 0) break;
161
+ if (current === "\n" && depth === 0) break;
162
+ }
163
+ const trimmed = statement.trim();
164
+ if (trimmed) statements.push(trimmed);
55
165
  }
56
- return false;
166
+ return statements;
57
167
  };
58
168
 
59
- const buildLines = (text) => {
60
- const lines = [];
61
- let start = 0;
62
- for (const line of text.split("\n")) {
63
- const indent = line.match(/^\s*/)?.[0].length ?? 0;
64
- lines.push({
65
- start,
66
- end: start + line.length,
67
- indent,
68
- text: line,
69
- });
70
- start += line.length + 1;
71
- }
72
- return lines;
73
- };
169
+ const lineNumberForIndex = (text, index) =>
170
+ text.slice(0, index).split("\n").length;
74
171
 
75
- const isInsidePythonTryBlock = (text, callIndex) => {
76
- const lines = buildLines(text);
77
- const callLineIndex = lines.findIndex(
78
- (line) => line.start <= callIndex && callIndex <= line.end,
79
- );
80
- if (callLineIndex < 0) return false;
81
- const callLine = lines[callLineIndex];
82
- for (let index = callLineIndex - 1; index >= 0; index -= 1) {
83
- const candidate = lines[index];
84
- if (!candidate.text.trim()) continue;
85
- if (!/^\s*try\s*:\s*(?:#.*)?$/.test(candidate.text)) continue;
86
- if (candidate.indent >= callLine.indent) continue;
87
- const escaped = lines
88
- .slice(index + 1, callLineIndex)
89
- .some(
90
- (line) =>
91
- line.text.trim() &&
92
- line.indent <= candidate.indent &&
93
- !/^\s*(?:except|finally|else)\b/.test(line.text),
94
- );
95
- if (!escaped) return true;
96
- }
97
- return false;
172
+ const isAwaitedOnCallLine = (text, callIndex) => {
173
+ const lineStart = text.lastIndexOf("\n", callIndex - 1) + 1;
174
+ return /\bawait\b/.test(text.slice(lineStart, callIndex));
98
175
  };
99
176
 
100
- const hasCatchHandlerOnCall = (text, callIndex) => {
101
- const openParenIndex = text.indexOf("(", callIndex);
102
- const closeParenIndex = findMatchingDelimiter(text, openParenIndex, "(", ")");
103
- if (closeParenIndex < 0) return false;
104
- return /^\s*\.catch\s*\(/.test(text.slice(closeParenIndex + 1));
177
+ const canonicalLifecycleApiName = (apiName) => {
178
+ if (apiName === "clue_init_fastapi" || apiName === "clue_init_django") {
179
+ return "ClueInit";
180
+ }
181
+ return apiName;
105
182
  };
106
183
 
107
- const isGuardedLifecycleCall = (text, callIndex) =>
108
- hasCatchHandlerOnCall(text, callIndex) ||
109
- isInsideSafeHelperCall(text, callIndex) ||
110
- isInsideJsTryBlock(text, callIndex) ||
111
- isInsidePythonTryBlock(text, callIndex);
112
-
113
184
  export const findLifecycleGuardViolations = (text) => {
114
185
  const violations = [];
115
186
  for (const match of text.matchAll(LIFECYCLE_CALL_PATTERN)) {
116
187
  const callIndex = match.index ?? 0;
117
- const apiName = match[1];
188
+ const apiName = canonicalLifecycleApiName(match[1]);
118
189
  if (isAwaitedOnCallLine(text, callIndex)) {
119
190
  violations.push({
120
191
  api_name: apiName,
121
192
  line: lineNumberForIndex(text, callIndex),
122
193
  reason: "awaited_lifecycle_call",
123
194
  });
124
- continue;
125
- }
126
- if (!isGuardedLifecycleCall(text, callIndex)) {
127
- violations.push({
128
- api_name: apiName,
129
- line: lineNumberForIndex(text, callIndex),
130
- reason: "unguarded_lifecycle_call",
131
- });
132
195
  }
133
196
  }
134
197
  return violations;
@@ -136,6 +199,8 @@ export const findLifecycleGuardViolations = (text) => {
136
199
 
137
200
  export const findLifecycleCallApiNames = (text) => [
138
201
  ...new Set(
139
- [...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) => match[1]),
202
+ [...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) =>
203
+ canonicalLifecycleApiName(match[1]),
204
+ ),
140
205
  ),
141
206
  ];