@produtype/core 1.9.1 → 1.10.0
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/dist/utils/textSearch.js +89 -15
- package/package.json +2 -2
package/dist/utils/textSearch.js
CHANGED
|
@@ -26,6 +26,36 @@ const blockComments_1 = require("../analyzer/blockComments");
|
|
|
26
26
|
* and is left alone.
|
|
27
27
|
*/
|
|
28
28
|
const PATTERN_DECLARATION = /^(?:const\s+\w+(?:\s*:[^=]+)?\s*=\s*)?\/(?:[^/\\]|\\.)+\/[gimsuy]*\s*[,;]?$/;
|
|
29
|
+
/**
|
|
30
|
+
* A name given a type, rather than a value.
|
|
31
|
+
*
|
|
32
|
+
* pocketbase's report cited three lines for its cross-origin policy: `AllowedOrigins
|
|
33
|
+
* []string` — a field in a Go struct — `var allowedOrigins []string` in the command
|
|
34
|
+
* that parses the flag, and `allowedOrigins: Array<string>` in a generated `.d.ts`.
|
|
35
|
+
* Not one of them is a decision about origins. The line that is,
|
|
36
|
+
* `config.AllowedOrigins = []string{"*"}` twenty lines further down, was never
|
|
37
|
+
* reached, so a reader was given three declarations to argue with instead of the
|
|
38
|
+
* default that actually applies.
|
|
39
|
+
*
|
|
40
|
+
* It is the same rule as the regex table above, for the same reason: a declaration
|
|
41
|
+
* says what a thing is, and this analyzer claims to say what the code does.
|
|
42
|
+
*
|
|
43
|
+
* The right-hand side has to look like a type, not a value, because `AllowOrigins:
|
|
44
|
+
* config.AllowedOrigins,` is a use and reads almost identically. A type starts with a
|
|
45
|
+
* capital or is one of the primitives, and carries no quotes, no call and no digits —
|
|
46
|
+
* and an object-literal member ends in a comma, which a field signature does not.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* `import SwiftUI` has the shape and is not a declaration of anything.
|
|
50
|
+
*
|
|
51
|
+
* A keyword followed by a capitalised word reads exactly like a field given a type,
|
|
52
|
+
* and the first version of this rule hid every Swift and Kotlin import — which is how
|
|
53
|
+
* the native toolkits are detected, so an iOS application lost its interface. The
|
|
54
|
+
* statement keywords are excluded by name; `var`, `let` and `readonly` stay, because
|
|
55
|
+
* those really do introduce a declaration.
|
|
56
|
+
*/
|
|
57
|
+
const STATEMENT_KEYWORD = /^(?:import|package|from|export|return|case|new|type|class|struct|interface|enum|func|fun|def|public|private|protected|internal|throw|throws|extends|implements|use|using|namespace|module|require|await|yield|delete|typeof|instanceof|in|is|as|if|else|for|while|switch|do|try|catch|finally|with|assert|raise|lambda|val|const)\b/;
|
|
58
|
+
const TYPE_DECLARATION = /^(?:var\s+|let\s+|readonly\s+)?\w+\??\s*(?::\s*|\s+)(?:\[\]|\*|Array<|Map<|\bstring\b|\bnumber\b|\bboolean\b|\bbool\b|\bany\b|\bunknown\b|\bvoid\b|[A-Z])[\w.<>[\]|&\s]*;?$/;
|
|
29
59
|
/**
|
|
30
60
|
* Prose about the code, rather than the code.
|
|
31
61
|
*
|
|
@@ -56,9 +86,13 @@ const COMMENT_LINE = /^(?:\/\/|\/\*|\*\/?|#(?![![])(?!!)|<!--|--\s)/;
|
|
|
56
86
|
*
|
|
57
87
|
* Nothing in the shape of the line tells the two apart, so the rule is not made.
|
|
58
88
|
*/
|
|
89
|
+
function declaresAType(trimmed) {
|
|
90
|
+
const withoutDeclarator = trimmed.replace(/^(?:var|let|readonly)\s+/, '');
|
|
91
|
+
return !STATEMENT_KEYWORD.test(withoutDeclarator) && TYPE_DECLARATION.test(trimmed);
|
|
92
|
+
}
|
|
59
93
|
function declaresRatherThanDoes(line) {
|
|
60
94
|
const trimmed = line.trim();
|
|
61
|
-
return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed);
|
|
95
|
+
return COMMENT_LINE.test(trimmed) || PATTERN_DECLARATION.test(trimmed) || declaresAType(trimmed);
|
|
62
96
|
}
|
|
63
97
|
/**
|
|
64
98
|
* Whether this line may be quoted back to the reader as something the code does.
|
|
@@ -93,6 +127,54 @@ function isCitableLine(line) {
|
|
|
93
127
|
* on is not worth the one it displaces.
|
|
94
128
|
*/
|
|
95
129
|
const MAX_CITABLE_LINE = 500;
|
|
130
|
+
/**
|
|
131
|
+
* A placeholder is not a value.
|
|
132
|
+
*
|
|
133
|
+
* pocketbase was reported as having tenant boundaries — `passed`, which raises a
|
|
134
|
+
* score — and the only two strong matches in its readable source were
|
|
135
|
+
* `"Ex. https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize"`,
|
|
136
|
+
* twice, in the help text of the form where somebody configures Microsoft sign-in.
|
|
137
|
+
* Microsoft Entra calls its directory a tenant; the string is telling a reader where
|
|
138
|
+
* to paste theirs. pocketbase has no organizations at all, and the rest of that
|
|
139
|
+
* finding was Apple's developer `teamId`, a weak word that cannot stand alone.
|
|
140
|
+
*
|
|
141
|
+
* `YOUR_SOMETHING` is the convention for "replace this", in documentation, in example
|
|
142
|
+
* configuration and in the help text beside a field. This analyzer already knows the
|
|
143
|
+
* shape: `your[_-]?secret` has been in the weak-secret list since the beginning.
|
|
144
|
+
*
|
|
145
|
+
* The token around the match is what decides, not the line. A line may hold a
|
|
146
|
+
* placeholder and a real value both, and only the matched one is being judged.
|
|
147
|
+
*/
|
|
148
|
+
const PLACEHOLDER_TOKEN = /^(?:your|my|sample|example|placeholder|changeme|todo|xxx+)[_-]/i;
|
|
149
|
+
function insideAPlaceholder(line, index, length) {
|
|
150
|
+
let start = index;
|
|
151
|
+
while (start > 0 && /[A-Za-z0-9_-]/.test(line[start - 1]))
|
|
152
|
+
start--;
|
|
153
|
+
let end = index + length;
|
|
154
|
+
while (end < line.length && /[A-Za-z0-9_-]/.test(line[end]))
|
|
155
|
+
end++;
|
|
156
|
+
return PLACEHOLDER_TOKEN.test(line.slice(start, end));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Where a needle first matches, or -1. Shared so that both searches below judge a
|
|
160
|
+
* match the same way.
|
|
161
|
+
*/
|
|
162
|
+
function findNeedle(line, needle) {
|
|
163
|
+
if (typeof needle === 'string') {
|
|
164
|
+
const index = line.indexOf(needle);
|
|
165
|
+
return index === -1 ? null : { index, length: needle.length };
|
|
166
|
+
}
|
|
167
|
+
const found = new RegExp(needle.source, needle.flags.replace('g', '')).exec(line);
|
|
168
|
+
return found ? { index: found.index, length: found[0].length } : null;
|
|
169
|
+
}
|
|
170
|
+
function matchesHere(line, needles) {
|
|
171
|
+
for (const needle of needles) {
|
|
172
|
+
const found = findNeedle(line, needle);
|
|
173
|
+
if (found && !insideAPlaceholder(line, found.index, found.length))
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
96
178
|
/**
|
|
97
179
|
* The lines of one already-read file that match, with the same hygiene the file
|
|
98
180
|
* search applies: no comments, no pattern tables, no minified lines.
|
|
@@ -123,12 +205,8 @@ function matchLines(text, needles, file = '') {
|
|
|
123
205
|
continue;
|
|
124
206
|
if (declaresRatherThanDoes(line))
|
|
125
207
|
continue;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (hit) {
|
|
129
|
-
matches.push({ file, line: i + 1, snippet: line.trim().slice(0, 200) });
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
208
|
+
if (matchesHere(line, needles)) {
|
|
209
|
+
matches.push({ file, line: i + 1, snippet: line.trim().slice(0, 200) });
|
|
132
210
|
}
|
|
133
211
|
}
|
|
134
212
|
return matches;
|
|
@@ -171,14 +249,10 @@ async function searchInFiles(root, files, needles, limit = 25, keep) {
|
|
|
171
249
|
continue;
|
|
172
250
|
if (declaresRatherThanDoes(line))
|
|
173
251
|
continue;
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
if (!keep || keep(match))
|
|
179
|
-
matches.push(match);
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
252
|
+
if (matchesHere(line, needles)) {
|
|
253
|
+
const match = { file, line: i + 1, snippet: line.trim().slice(0, 200) };
|
|
254
|
+
if (!keep || keep(match))
|
|
255
|
+
matches.push(match);
|
|
182
256
|
}
|
|
183
257
|
if (matches.length >= limit)
|
|
184
258
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"docs:stacks": "npm run build && node scripts/sync-readme-stacks.mjs",
|
|
38
38
|
"prepare": "npm run build",
|
|
39
39
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
40
|
-
"prepublishOnly": "npm run build && node scripts/assert-no-ai.mjs && npm test"
|
|
40
|
+
"prepublishOnly": "npm run build && npm run lint && node scripts/assert-no-ai.mjs && npm test"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=18"
|