@ttsc/evidence 0.25.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/LICENSE +21 -0
- package/README.md +279 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +87 -0
- package/lib/internal/loadPrismaModels.d.ts +1 -0
- package/lib/internal/loadPrismaModels.js +197 -0
- package/lib/internal/loadSwaggerOperations.d.ts +1 -0
- package/lib/internal/loadSwaggerOperations.js +156 -0
- package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
- package/lib/internal/normalizeSwaggerDocument.js +7 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
- package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
- package/lib/structures/index.d.ts +13 -0
- package/lib/structures/index.js +29 -0
- package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
- package/lib/typings/ITtscLintContributorRules.js +2 -0
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
- package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
- package/lib/typings/index.d.ts +5 -0
- package/lib/typings/index.js +21 -0
- package/native/base.go +298 -0
- package/native/config.go +798 -0
- package/native/declaration.go +240 -0
- package/native/documented.go +353 -0
- package/native/exports.go +484 -0
- package/native/glob.go +207 -0
- package/native/graph.go +1409 -0
- package/native/health.go +45 -0
- package/native/hints.go +261 -0
- package/native/imports.go +373 -0
- package/native/inputs.go +125 -0
- package/native/loader.go +424 -0
- package/native/markdown.go +423 -0
- package/native/model.go +296 -0
- package/native/prisma.go +719 -0
- package/native/prisma_cache.go +213 -0
- package/native/prisma_locator.go +417 -0
- package/native/singular.go +369 -0
- package/native/swagger.go +331 -0
- package/native/swagger_cache.go +274 -0
- package/native/todo.go +138 -0
- package/native/typescript.go +1199 -0
- package/native/typescript_cache.go +96 -0
- package/package.json +64 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
9
|
+
|
|
10
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// singularRule enforces that a file declares exactly one public identity and
|
|
14
|
+
// takes that identity's name.
|
|
15
|
+
//
|
|
16
|
+
// The unit counted is an identity, not an export. TypeScript declaration
|
|
17
|
+
// merging exposes one name through several declarations — `export interface I`
|
|
18
|
+
// beside `export namespace I`, `export const x` beside `export default x` — and
|
|
19
|
+
// every one of those forms is a single identity here, so the permitted merges
|
|
20
|
+
// fall out of what is counted rather than being carved out of it.
|
|
21
|
+
type singularRule struct{}
|
|
22
|
+
|
|
23
|
+
func (singularRule) Name() string { return singularRuleName }
|
|
24
|
+
|
|
25
|
+
func (singularRule) Visits() []shimast.Kind {
|
|
26
|
+
return []shimast.Kind{shimast.KindSourceFile}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func (singularRule) NeedsTypeChecker() bool { return false }
|
|
30
|
+
|
|
31
|
+
func (singularRule) VisitsDeclarationFiles() bool { return false }
|
|
32
|
+
|
|
33
|
+
func (singularRule) AcceptsTtscLintOptions() bool { return false }
|
|
34
|
+
|
|
35
|
+
func (singularRule) Check(ctx *rule.Context, node *shimast.Node) {
|
|
36
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
if node.Kind != shimast.KindSourceFile {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
surface := collectPublicSurface(ctx.File)
|
|
43
|
+
content := ctx.File.Text()
|
|
44
|
+
|
|
45
|
+
// One finding per file, most structural first. A file that declares two
|
|
46
|
+
// identities has to be split before its name can mean anything, and an
|
|
47
|
+
// anonymous default has to be named before a name can be compared.
|
|
48
|
+
if len(surface.Identities) > 1 {
|
|
49
|
+
ctx.Report(
|
|
50
|
+
surface.Identities[1].Node,
|
|
51
|
+
"A file declares exactly one public identity, but this one declares "+
|
|
52
|
+
decimal(len(surface.Identities))+": "+
|
|
53
|
+
describeIdentities(surface.Identities, content)+
|
|
54
|
+
". Move every extra identity to its own file, or re-export them from a barrel; merged declarations of one name count once.",
|
|
55
|
+
)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if surface.AnonymousDefault != nil {
|
|
59
|
+
ctx.Report(
|
|
60
|
+
surface.AnonymousDefault,
|
|
61
|
+
"An anonymous default export has no name for its file to take. Name the exported declaration, or export a named declaration instead of an anonymous default.",
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if len(surface.Identities) == 0 {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
base, extension := splitModuleBaseName(ctx.File.FileName())
|
|
70
|
+
if base == "index" {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
identity := surface.Identities[0]
|
|
74
|
+
names := identity.matchable()
|
|
75
|
+
for _, name := range names {
|
|
76
|
+
if name == base {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
ctx.Report(
|
|
81
|
+
identity.Node,
|
|
82
|
+
"A file takes the name of its public identity, but '"+base+extension+
|
|
83
|
+
"' declares '"+strings.Join(names, "' / '")+"'. Rename the file to '"+
|
|
84
|
+
names[0]+extension+"', or rename the identity to '"+base+"'.",
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func init() { rule.Register(singularRule{}) }
|
|
89
|
+
|
|
90
|
+
// publicIdentity is one locally declared name that this file exposes.
|
|
91
|
+
//
|
|
92
|
+
// Addresses holds the public names a consumer can import, which is what the
|
|
93
|
+
// file name must match. It excludes `default`, which names no identity: a file
|
|
94
|
+
// whose only exposure is `export default x` is still the file of `x`.
|
|
95
|
+
type publicIdentity struct {
|
|
96
|
+
Local string
|
|
97
|
+
Addresses []string
|
|
98
|
+
Node *shimast.Node
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func (identity publicIdentity) matchable() []string {
|
|
102
|
+
if len(identity.Addresses) != 0 {
|
|
103
|
+
return identity.Addresses
|
|
104
|
+
}
|
|
105
|
+
return []string{identity.Local}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type publicSurface struct {
|
|
109
|
+
Identities []publicIdentity
|
|
110
|
+
AnonymousDefault *shimast.Node
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// collectPublicSurface reduces a source file's top-level statements to the
|
|
114
|
+
// identities it owns.
|
|
115
|
+
//
|
|
116
|
+
// Ownership is declaration ownership, never mere re-exposure. An export
|
|
117
|
+
// declaration carrying a module specifier declares nothing here, so a barrel
|
|
118
|
+
// owns no identity and needs no exemption; an export list or default assignment
|
|
119
|
+
// naming an import binding is the same case one step removed.
|
|
120
|
+
func collectPublicSurface(file *shimast.SourceFile) publicSurface {
|
|
121
|
+
surface := publicSurface{}
|
|
122
|
+
if file == nil || file.Statements == nil {
|
|
123
|
+
return surface
|
|
124
|
+
}
|
|
125
|
+
order := []string{}
|
|
126
|
+
nodes := map[string]*shimast.Node{}
|
|
127
|
+
addresses := map[string]map[string]bool{}
|
|
128
|
+
owned := map[string]bool{}
|
|
129
|
+
|
|
130
|
+
declare := func(name string, node *shimast.Node) {
|
|
131
|
+
if name == "" {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if _, exists := nodes[name]; !exists {
|
|
135
|
+
order = append(order, name)
|
|
136
|
+
nodes[name] = node
|
|
137
|
+
addresses[name] = map[string]bool{}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
expose := func(local string, public string) {
|
|
141
|
+
if _, exists := nodes[local]; !exists {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
owned[local] = true
|
|
145
|
+
if public != "" {
|
|
146
|
+
addresses[local][public] = true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for _, statement := range file.Statements.Nodes {
|
|
151
|
+
if statement == nil {
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
for _, declared := range topLevelDeclaredNames(statement) {
|
|
155
|
+
declare(declared.Name, declared.Node)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for _, statement := range file.Statements.Nodes {
|
|
159
|
+
if statement == nil {
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
switch statement.Kind {
|
|
163
|
+
case shimast.KindExportDeclaration:
|
|
164
|
+
for _, alias := range localExportAliases(statement) {
|
|
165
|
+
expose(alias.Local, alias.Public)
|
|
166
|
+
}
|
|
167
|
+
case shimast.KindExportAssignment:
|
|
168
|
+
assignment := statement.AsExportAssignment()
|
|
169
|
+
if assignment == nil {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
local := ""
|
|
173
|
+
if assignment.Expression != nil &&
|
|
174
|
+
assignment.Expression.Kind == shimast.KindIdentifier {
|
|
175
|
+
local = declarationName(assignment.Expression)
|
|
176
|
+
}
|
|
177
|
+
if local == "" || nodes[local] == nil {
|
|
178
|
+
// `export default 1` and `export default fromAnotherModule` both
|
|
179
|
+
// expose something this file cannot be named after.
|
|
180
|
+
if local == "" {
|
|
181
|
+
surface.AnonymousDefault = statement
|
|
182
|
+
}
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
expose(local, "")
|
|
186
|
+
default:
|
|
187
|
+
if !isSyntacticallyExported(statement) {
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
declared := topLevelDeclaredNames(statement)
|
|
191
|
+
for _, name := range declared {
|
|
192
|
+
expose(name.Name, name.Name)
|
|
193
|
+
}
|
|
194
|
+
if len(declared) == 0 && isDefaultExported(statement) {
|
|
195
|
+
surface.AnonymousDefault = statement
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for _, local := range order {
|
|
201
|
+
if !owned[local] {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
public := make([]string, 0, len(addresses[local]))
|
|
205
|
+
for name := range addresses[local] {
|
|
206
|
+
public = append(public, name)
|
|
207
|
+
}
|
|
208
|
+
sort.Strings(public)
|
|
209
|
+
surface.Identities = append(surface.Identities, publicIdentity{
|
|
210
|
+
Local: local,
|
|
211
|
+
Addresses: public,
|
|
212
|
+
Node: nodes[local],
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
return surface
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// declaredName pairs a module-scope name with the identifier that spells it.
|
|
219
|
+
//
|
|
220
|
+
// The identifier is kept rather than the statement because both the reported
|
|
221
|
+
// range and the reported line come from it. A statement's own position starts
|
|
222
|
+
// before its leading trivia, so anchoring there underlines the blank line above
|
|
223
|
+
// the declaration and names the wrong line in the message.
|
|
224
|
+
type declaredName struct {
|
|
225
|
+
Name string
|
|
226
|
+
Node *shimast.Node
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// topLevelDeclaredNames returns the names a statement declares in module scope.
|
|
230
|
+
//
|
|
231
|
+
// Only identifier names qualify. A module declaration named by a string literal
|
|
232
|
+
// is an augmentation of another module rather than an identity of this one, and
|
|
233
|
+
// counting it would report every ambient declaration file as an extra identity.
|
|
234
|
+
func topLevelDeclaredNames(statement *shimast.Node) []declaredName {
|
|
235
|
+
switch statement.Kind {
|
|
236
|
+
case shimast.KindInterfaceDeclaration,
|
|
237
|
+
shimast.KindTypeAliasDeclaration,
|
|
238
|
+
shimast.KindFunctionDeclaration,
|
|
239
|
+
shimast.KindClassDeclaration,
|
|
240
|
+
shimast.KindEnumDeclaration,
|
|
241
|
+
shimast.KindModuleDeclaration:
|
|
242
|
+
name := statement.Name()
|
|
243
|
+
if name == nil || name.Kind != shimast.KindIdentifier {
|
|
244
|
+
return nil
|
|
245
|
+
}
|
|
246
|
+
if declared := declarationName(name); declared != "" {
|
|
247
|
+
return []declaredName{{Name: declared, Node: name}}
|
|
248
|
+
}
|
|
249
|
+
return nil
|
|
250
|
+
case shimast.KindVariableStatement:
|
|
251
|
+
variable := statement.AsVariableStatement()
|
|
252
|
+
if variable == nil || variable.DeclarationList == nil {
|
|
253
|
+
return nil
|
|
254
|
+
}
|
|
255
|
+
list := variable.DeclarationList.AsVariableDeclarationList()
|
|
256
|
+
if list == nil || list.Declarations == nil {
|
|
257
|
+
return nil
|
|
258
|
+
}
|
|
259
|
+
names := []declaredName{}
|
|
260
|
+
for _, declaration := range list.Declarations.Nodes {
|
|
261
|
+
if declaration == nil {
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
for _, binding := range bindingIdentifierNodes(declaration.Name()) {
|
|
265
|
+
if name := declarationName(binding); name != "" {
|
|
266
|
+
names = append(names, declaredName{Name: name, Node: binding})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return names
|
|
271
|
+
default:
|
|
272
|
+
return nil
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// exportAlias is one entry of an export list: the local declaration it names,
|
|
277
|
+
// and the public name it gives that declaration.
|
|
278
|
+
//
|
|
279
|
+
// Public is empty for `export { x as default }`, which exposes `x` without
|
|
280
|
+
// giving it an addressable name — the same state `export default x` produces.
|
|
281
|
+
type exportAlias struct {
|
|
282
|
+
Local string
|
|
283
|
+
Public string
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// localExportAliases lists the exposures an export list creates, skipping any
|
|
287
|
+
// list that re-exports from another module.
|
|
288
|
+
//
|
|
289
|
+
// The result is a slice rather than a local-keyed map because one local may be
|
|
290
|
+
// exported under several names. Collapsing those to one would drop every name
|
|
291
|
+
// but the last, and a file legitimately named after a dropped name would then
|
|
292
|
+
// be reported.
|
|
293
|
+
func localExportAliases(statement *shimast.Node) []exportAlias {
|
|
294
|
+
aliases := []exportAlias{}
|
|
295
|
+
declaration := statement.AsExportDeclaration()
|
|
296
|
+
if declaration == nil ||
|
|
297
|
+
declaration.ModuleSpecifier != nil ||
|
|
298
|
+
declaration.ExportClause == nil ||
|
|
299
|
+
declaration.ExportClause.Kind != shimast.KindNamedExports {
|
|
300
|
+
return aliases
|
|
301
|
+
}
|
|
302
|
+
named := declaration.ExportClause.AsNamedExports()
|
|
303
|
+
if named == nil || named.Elements == nil {
|
|
304
|
+
return aliases
|
|
305
|
+
}
|
|
306
|
+
for _, element := range named.Elements.Nodes {
|
|
307
|
+
if element == nil || element.Kind != shimast.KindExportSpecifier {
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
specifier := element.AsExportSpecifier()
|
|
311
|
+
if specifier == nil {
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
localNode := specifier.PropertyName
|
|
315
|
+
if localNode == nil {
|
|
316
|
+
localNode = specifier.Name()
|
|
317
|
+
}
|
|
318
|
+
local := declarationName(localNode)
|
|
319
|
+
public := declarationName(specifier.Name())
|
|
320
|
+
if local == "" || public == "" {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
if public == "default" {
|
|
324
|
+
public = ""
|
|
325
|
+
}
|
|
326
|
+
aliases = append(aliases, exportAlias{Local: local, Public: public})
|
|
327
|
+
}
|
|
328
|
+
return aliases
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func isDefaultExported(node *shimast.Node) bool {
|
|
332
|
+
return node != nil &&
|
|
333
|
+
shimast.GetCombinedModifierFlags(node)&shimast.ModifierFlagsDefault != 0
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// splitModuleBaseName separates a module's base name from its final extension.
|
|
337
|
+
//
|
|
338
|
+
// Only the final extension is removed. A dotted infix such as `foo.test.ts`
|
|
339
|
+
// keeps `foo.test`, which no identifier can equal, so such a file is reported
|
|
340
|
+
// rather than silently matched against a prefix of itself.
|
|
341
|
+
func splitModuleBaseName(fileName string) (string, string) {
|
|
342
|
+
base := path.Base(strings.ReplaceAll(fileName, "\\", "/"))
|
|
343
|
+
extension := path.Ext(base)
|
|
344
|
+
return strings.TrimSuffix(base, extension), extension
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
func describeIdentities(identities []publicIdentity, content string) string {
|
|
348
|
+
described := make([]string, 0, len(identities))
|
|
349
|
+
for _, identity := range identities {
|
|
350
|
+
described = append(
|
|
351
|
+
described,
|
|
352
|
+
"'"+strings.Join(identity.matchable(), "' / '")+"' (line "+
|
|
353
|
+
decimal(lineAt(content, identityOffset(identity.Node)))+")",
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
return strings.Join(described, ", ")
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// identityOffset points at the last character of a declared name.
|
|
360
|
+
//
|
|
361
|
+
// A node's own start position sits before its leading trivia, so the end is
|
|
362
|
+
// what keeps a reported line on the declaration rather than on the blank line
|
|
363
|
+
// above it.
|
|
364
|
+
func identityOffset(node *shimast.Node) int {
|
|
365
|
+
if node == nil || node.End() <= 0 {
|
|
366
|
+
return 0
|
|
367
|
+
}
|
|
368
|
+
return node.End() - 1
|
|
369
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"net/url"
|
|
9
|
+
"os"
|
|
10
|
+
"os/exec"
|
|
11
|
+
"sort"
|
|
12
|
+
"strings"
|
|
13
|
+
"time"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const swaggerBridgeTimeout = 60 * time.Second
|
|
17
|
+
const swaggerBridgeOutputLimit = 64 * 1024 * 1024
|
|
18
|
+
const swaggerBridgeErrorLimit = 64 * 1024
|
|
19
|
+
|
|
20
|
+
const swaggerBridgeScript = `
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
const { createRequire } = require("node:module");
|
|
23
|
+
|
|
24
|
+
const root = process.argv[1];
|
|
25
|
+
const projectRequire = createRequire(path.join(root, "package.json"));
|
|
26
|
+
const manifest = projectRequire.resolve("@ttsc/evidence/package.json");
|
|
27
|
+
const pluginRequire = createRequire(manifest);
|
|
28
|
+
const normalizer = pluginRequire(
|
|
29
|
+
path.join(path.dirname(manifest), "lib", "internal", "loadSwaggerOperations.js"),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
let input = "";
|
|
33
|
+
process.stdin.setEncoding("utf8");
|
|
34
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
35
|
+
process.stdin.on("end", async () => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await normalizer.loadSwaggerOperations(JSON.parse(input));
|
|
38
|
+
process.stdout.write(JSON.stringify(result));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
process.stderr.write(error instanceof Error ? error.message : String(error));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
type swaggerNormalizationRequest struct {
|
|
47
|
+
Root string `json:"root"`
|
|
48
|
+
Sources []string `json:"sources"`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type swaggerNormalizationResult struct {
|
|
52
|
+
Documents []swaggerDocumentInventory `json:"documents"`
|
|
53
|
+
Problems []swaggerDocumentProblem `json:"problems"`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// swaggerDocumentInventory is one normalized document.
|
|
57
|
+
//
|
|
58
|
+
// Digest is the SHA-256 of the bytes the normalizer itself read, and it is what
|
|
59
|
+
// the result is remembered under. Hashing here rather than trusting the digest
|
|
60
|
+
// this process took beforehand is what makes the key exact: the normalizer runs
|
|
61
|
+
// in another process and opens the file again, so a write landing between the
|
|
62
|
+
// two reads would otherwise bind one document's operations to another
|
|
63
|
+
// document's bytes — an entry that answers a later cycle with the wrong
|
|
64
|
+
// document, forever.
|
|
65
|
+
//
|
|
66
|
+
// It is empty for a remote source, which never participates in reuse.
|
|
67
|
+
type swaggerDocumentInventory struct {
|
|
68
|
+
Source string `json:"source"`
|
|
69
|
+
Operations []swaggerOperation `json:"operations"`
|
|
70
|
+
Digest string `json:"digest"`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// swaggerDocumentProblem is one source the normalizer refused. Digest carries
|
|
74
|
+
// the same meaning as on an inventory, so a document that cannot be normalized
|
|
75
|
+
// is remembered as a failure rather than re-normalized on every later cycle.
|
|
76
|
+
type swaggerDocumentProblem struct {
|
|
77
|
+
Source string `json:"source"`
|
|
78
|
+
Message string `json:"message"`
|
|
79
|
+
Digest string `json:"digest"`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type swaggerOperation struct {
|
|
83
|
+
Method string `json:"method"`
|
|
84
|
+
Path string `json:"path"`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func loadSwaggerInventories(
|
|
88
|
+
root string,
|
|
89
|
+
config graphConfig,
|
|
90
|
+
) (map[string]*artifactInventory, []string) {
|
|
91
|
+
sources := configuredSwaggerSources(config)
|
|
92
|
+
inventories := map[string]*artifactInventory{}
|
|
93
|
+
for _, source := range sources {
|
|
94
|
+
inventories[source] = &artifactInventory{
|
|
95
|
+
Path: source,
|
|
96
|
+
Type: artifactSwagger,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if len(sources) == 0 {
|
|
100
|
+
return inventories, nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Normalizing a document costs a Node process, and the process start
|
|
104
|
+
// dominates the parse — a three-operation document and a
|
|
105
|
+
// two-hundred-operation one pay nearly the same. A resident host repeats
|
|
106
|
+
// this every cycle, so an unchanged document is re-normalized on every
|
|
107
|
+
// TypeScript keystroke that triggers a rebuild.
|
|
108
|
+
digests := swaggerContentDigests(root, sources)
|
|
109
|
+
pending := []string{}
|
|
110
|
+
problems := []string{}
|
|
111
|
+
for _, source := range sources {
|
|
112
|
+
outcome, hit := lookupSwaggerDocument(source, digests[source])
|
|
113
|
+
if !hit {
|
|
114
|
+
pending = append(pending, source)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
problems = append(
|
|
118
|
+
problems,
|
|
119
|
+
swaggerUnitsFromOutcome(source, inventories[source], outcome)...,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if len(pending) == 0 {
|
|
123
|
+
return inventories, problems
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
result, err := normalizeSwaggerSources(root, pending)
|
|
127
|
+
if err != nil {
|
|
128
|
+
message := "Evidence graph could not run its Swagger normalizer: " + err.Error() + ". Swagger references require Node.js and the installed @typia/interface, @typia/utils, and yaml dependencies."
|
|
129
|
+
for _, source := range pending {
|
|
130
|
+
inventories[source].LoadFailed = true
|
|
131
|
+
inventories[source].Problems = append(
|
|
132
|
+
inventories[source].Problems,
|
|
133
|
+
inventoryProblem{Symbol: "operation", Message: message},
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return inventories, append(problems, message)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
seen := map[string]bool{}
|
|
140
|
+
for _, document := range result.Documents {
|
|
141
|
+
inventory := inventories[document.Source]
|
|
142
|
+
if inventory == nil {
|
|
143
|
+
problems = append(
|
|
144
|
+
problems,
|
|
145
|
+
"Evidence graph Swagger normalizer returned an unconfigured source '"+displaySwaggerSource(document.Source)+"'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if seen[document.Source] {
|
|
150
|
+
problems = append(
|
|
151
|
+
problems,
|
|
152
|
+
"Evidence graph Swagger normalizer returned source '"+displaySwaggerSource(document.Source)+"' more than once. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
|
|
153
|
+
)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
seen[document.Source] = true
|
|
157
|
+
outcome := swaggerDocumentOutcome{Operations: document.Operations}
|
|
158
|
+
problems = append(
|
|
159
|
+
problems,
|
|
160
|
+
swaggerUnitsFromOutcome(document.Source, inventory, outcome)...,
|
|
161
|
+
)
|
|
162
|
+
rememberSwaggerDocument(document.Source, document.Digest, outcome)
|
|
163
|
+
}
|
|
164
|
+
for _, problem := range result.Problems {
|
|
165
|
+
inventory := inventories[problem.Source]
|
|
166
|
+
if inventory == nil {
|
|
167
|
+
problems = append(
|
|
168
|
+
problems,
|
|
169
|
+
"Evidence graph Swagger normalizer rejected an unconfigured source '"+displaySwaggerSource(problem.Source)+"'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
|
|
170
|
+
)
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
seen[problem.Source] = true
|
|
174
|
+
outcome := swaggerDocumentOutcome{
|
|
175
|
+
Rejected: true,
|
|
176
|
+
Problem: problem.Message,
|
|
177
|
+
}
|
|
178
|
+
problems = append(
|
|
179
|
+
problems,
|
|
180
|
+
swaggerUnitsFromOutcome(problem.Source, inventory, outcome)...,
|
|
181
|
+
)
|
|
182
|
+
rememberSwaggerDocument(problem.Source, problem.Digest, outcome)
|
|
183
|
+
}
|
|
184
|
+
for _, source := range pending {
|
|
185
|
+
if seen[source] {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
message := "Evidence graph Swagger normalizer returned no result for '" + displaySwaggerSource(source) + "'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree."
|
|
189
|
+
inventories[source].LoadFailed = true
|
|
190
|
+
inventories[source].Problems = append(
|
|
191
|
+
inventories[source].Problems,
|
|
192
|
+
inventoryProblem{Symbol: "operation", Message: message},
|
|
193
|
+
)
|
|
194
|
+
problems = append(problems, message)
|
|
195
|
+
}
|
|
196
|
+
return inventories, problems
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func configuredSwaggerSources(config graphConfig) []string {
|
|
200
|
+
unique := map[string]bool{}
|
|
201
|
+
sources := []string{}
|
|
202
|
+
for _, claim := range config.Claims {
|
|
203
|
+
for _, reference := range claim.References {
|
|
204
|
+
if reference.Type != artifactSwagger {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if unique[reference.Source] {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
unique[reference.Source] = true
|
|
211
|
+
sources = append(sources, reference.Source)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
sort.Strings(sources)
|
|
215
|
+
return sources
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func normalizeSwaggerSources(
|
|
219
|
+
root string,
|
|
220
|
+
sources []string,
|
|
221
|
+
) (swaggerNormalizationResult, error) {
|
|
222
|
+
request, err := json.Marshal(swaggerNormalizationRequest{
|
|
223
|
+
Root: root,
|
|
224
|
+
Sources: sources,
|
|
225
|
+
})
|
|
226
|
+
if err != nil {
|
|
227
|
+
return swaggerNormalizationResult{}, err
|
|
228
|
+
}
|
|
229
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
230
|
+
if node == "" {
|
|
231
|
+
node, err = exec.LookPath("node")
|
|
232
|
+
if err != nil {
|
|
233
|
+
return swaggerNormalizationResult{}, errors.New("Node.js executable was not found")
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
ctx, cancel := context.WithTimeout(context.Background(), swaggerBridgeTimeout)
|
|
237
|
+
defer cancel()
|
|
238
|
+
command := exec.CommandContext(ctx, node, "-e", swaggerBridgeScript, root)
|
|
239
|
+
command.Dir = root
|
|
240
|
+
command.Stdin = bytes.NewReader(request)
|
|
241
|
+
stdout := &limitedBuffer{Limit: swaggerBridgeOutputLimit}
|
|
242
|
+
stderr := &limitedBuffer{Limit: swaggerBridgeErrorLimit}
|
|
243
|
+
command.Stdout = stdout
|
|
244
|
+
command.Stderr = stderr
|
|
245
|
+
if err := command.Run(); err != nil {
|
|
246
|
+
if ctx.Err() == context.DeadlineExceeded {
|
|
247
|
+
return swaggerNormalizationResult{}, errors.New("Swagger normalizer exceeded its 60 second timeout")
|
|
248
|
+
}
|
|
249
|
+
detail := strings.TrimSpace(stderr.String())
|
|
250
|
+
if detail == "" {
|
|
251
|
+
detail = err.Error()
|
|
252
|
+
}
|
|
253
|
+
return swaggerNormalizationResult{}, errors.New(detail)
|
|
254
|
+
}
|
|
255
|
+
var result swaggerNormalizationResult
|
|
256
|
+
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
|
257
|
+
return swaggerNormalizationResult{}, errors.New("Swagger normalizer returned invalid JSON: " + err.Error())
|
|
258
|
+
}
|
|
259
|
+
return result, nil
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func swaggerOperationUnit(
|
|
263
|
+
source string,
|
|
264
|
+
operation swaggerOperation,
|
|
265
|
+
) (*evidenceUnit, string) {
|
|
266
|
+
method := strings.TrimSpace(operation.Method)
|
|
267
|
+
operationPath := operation.Path
|
|
268
|
+
if method == "" || strings.ContainsAny(method, ":\t\r\n ") {
|
|
269
|
+
return nil, "Swagger source '" + displaySwaggerSource(source) + "' contains an operation method that cannot form a '<METHOD>:<path>' evidence target."
|
|
270
|
+
}
|
|
271
|
+
if !strings.HasPrefix(operationPath, "/") || containsWhitespace(operationPath) {
|
|
272
|
+
return nil, "Swagger source '" + displaySwaggerSource(source) + "' contains operation path '" + operationPath + "', which cannot form a whitespace-free '<METHOD>:<path>' evidence target."
|
|
273
|
+
}
|
|
274
|
+
target := strings.ToUpper(method) + ":" + operationPath
|
|
275
|
+
readable := "Swagger operation '" + strings.ToUpper(method) + " " + operationPath + "'"
|
|
276
|
+
return &evidenceUnit{
|
|
277
|
+
ID: "swagger:" + source + ":" + target,
|
|
278
|
+
Target: target,
|
|
279
|
+
Type: artifactSwagger,
|
|
280
|
+
Symbol: "operation",
|
|
281
|
+
Path: displaySwaggerSource(source),
|
|
282
|
+
Readable: readable,
|
|
283
|
+
}, ""
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// isRemoteSwaggerSource reports whether a normalized Swagger source names a URL
|
|
287
|
+
// rather than a project-relative file.
|
|
288
|
+
//
|
|
289
|
+
// Two callers depend on it and both fail quietly when it is wrong: a remote
|
|
290
|
+
// source declared as a project input is rejected by the host and takes the
|
|
291
|
+
// whole snapshot down with it, and a remote source admitted to the content
|
|
292
|
+
// cache would be answered from bytes that were never fetched.
|
|
293
|
+
//
|
|
294
|
+
// The scheme is read through `url.Parse` rather than matched as a prefix
|
|
295
|
+
// because `normalizeSwaggerSource` stores the author's spelling, and a scheme
|
|
296
|
+
// is case-insensitive — `HTTPS://host/s.json` is accepted there and would slip
|
|
297
|
+
// past a literal `https://` comparison.
|
|
298
|
+
func isRemoteSwaggerSource(source string) bool {
|
|
299
|
+
parsed, err := url.Parse(source)
|
|
300
|
+
return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func displaySwaggerSource(source string) string {
|
|
304
|
+
parsed, err := url.Parse(source)
|
|
305
|
+
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
|
306
|
+
return source
|
|
307
|
+
}
|
|
308
|
+
if parsed.User != nil {
|
|
309
|
+
parsed.User = url.User("***")
|
|
310
|
+
}
|
|
311
|
+
if parsed.RawQuery != "" {
|
|
312
|
+
parsed.RawQuery = "<redacted>"
|
|
313
|
+
}
|
|
314
|
+
return parsed.String()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
type limitedBuffer struct {
|
|
318
|
+
bytes.Buffer
|
|
319
|
+
Limit int
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func (buffer *limitedBuffer) Write(content []byte) (int, error) {
|
|
323
|
+
if buffer.Len()+len(content) > buffer.Limit {
|
|
324
|
+
remaining := buffer.Limit - buffer.Len()
|
|
325
|
+
if remaining > 0 {
|
|
326
|
+
_, _ = buffer.Buffer.Write(content[:remaining])
|
|
327
|
+
}
|
|
328
|
+
return len(content), errors.New("process output exceeded its limit")
|
|
329
|
+
}
|
|
330
|
+
return buffer.Buffer.Write(content)
|
|
331
|
+
}
|