@ttsc/lint 0.26.2 → 0.28.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/lib/index.d.ts +5 -1
- package/lib/index.js +458 -112
- package/lib/index.js.map +1 -1
- package/linthost/compile.go +147 -3
- package/linthost/config.go +230 -25
- package/linthost/dispatch.go +3 -1
- package/linthost/graph_nodes.go +205 -0
- package/linthost/hints.go +1 -1
- package/linthost/project_inputs.go +15 -4
- package/linthost/serve.go +19 -0
- package/package.json +2 -2
- package/rule/graph.go +150 -0
- package/src/index.ts +511 -115
package/linthost/serve.go
CHANGED
|
@@ -235,9 +235,11 @@ func RunLSPServe(in io.Reader, out io.Writer, args []string) int {
|
|
|
235
235
|
return 2
|
|
236
236
|
}
|
|
237
237
|
residentPrograms = newResidentProgramCache()
|
|
238
|
+
residentRules = &residentRuleCache{}
|
|
238
239
|
defer func() {
|
|
239
240
|
residentPrograms.invalidate()
|
|
240
241
|
residentPrograms = nil
|
|
242
|
+
residentRules = nil
|
|
241
243
|
}()
|
|
242
244
|
encoder := json.NewEncoder(out)
|
|
243
245
|
// ReadString imposes no line-length limit: a request is small, but keeping the
|
|
@@ -276,6 +278,12 @@ func handleServeLSPLine(line string, base *lspCommandOptions, encoder *json.Enco
|
|
|
276
278
|
return
|
|
277
279
|
}
|
|
278
280
|
if req.Invalidate {
|
|
281
|
+
// The Program only. The rule memo validates itself against the files it was
|
|
282
|
+
// loaded from, so it needs no help from a client — and dropping it here
|
|
283
|
+
// would re-evaluate the project's configuration on exactly the requests
|
|
284
|
+
// that carry this control, which is every request from a consumer that
|
|
285
|
+
// cannot localize a source change. That is the cost the memo exists to
|
|
286
|
+
// remove.
|
|
279
287
|
residentPrograms.invalidate()
|
|
280
288
|
}
|
|
281
289
|
if len(req.Changed) > 0 {
|
|
@@ -310,6 +318,17 @@ func handleServeLSPLine(line string, base *lspCommandOptions, encoder *json.Enco
|
|
|
310
318
|
case "lsp-hints":
|
|
311
319
|
result, code := computeLSPHints(&opts)
|
|
312
320
|
encodeServeResult(encoder, result, code)
|
|
321
|
+
case "graph-nodes":
|
|
322
|
+
// Neither of these describes a document, so neither carries a uri. They
|
|
323
|
+
// describe the project, and they join the daemon for the reason the other
|
|
324
|
+
// read verbs did: a consumer that asks them again whenever a file it
|
|
325
|
+
// watches moves would otherwise pay a process, a plugin load, and a Program
|
|
326
|
+
// for every edit.
|
|
327
|
+
result, code := computeGraphNodes(&opts)
|
|
328
|
+
encodeServeResult(encoder, result, code)
|
|
329
|
+
case "project-inputs":
|
|
330
|
+
result, code := computeProjectInputs(&opts)
|
|
331
|
+
encodeServeResult(encoder, result, code)
|
|
313
332
|
case "lsp-command-ids":
|
|
314
333
|
encodeServeResult(encoder, lspCommandIDs(), 0)
|
|
315
334
|
case "lsp-code-action-kinds":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Reference ttsc plugin: ESLint-style lint rules over the TypeScript-Go Program used by the type-check pass.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@types/node": "^25.3.0",
|
|
38
38
|
"rimraf": "^6.1.2",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
|
-
"ttsc": "0.
|
|
40
|
+
"ttsc": "0.28.0"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
package/rule/graph.go
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package rule
|
|
2
|
+
|
|
3
|
+
import "encoding/json"
|
|
4
|
+
|
|
5
|
+
// GraphNodeKind names what a published graph node is.
|
|
6
|
+
//
|
|
7
|
+
// The vocabulary is fixed by the host rather than chosen by the contributor,
|
|
8
|
+
// because a consumer has to know what it received: a kind it does not recognize
|
|
9
|
+
// is a node it cannot rank, colour, or contain. A contributor publishing an
|
|
10
|
+
// unrecognized kind has its node dropped, and the drop is a declared outcome
|
|
11
|
+
// rather than a silent one.
|
|
12
|
+
//
|
|
13
|
+
// It is deliberately not evidence-specific. These are the shapes an artifact an
|
|
14
|
+
// author can cite actually has — a document and its sections, a data model and
|
|
15
|
+
// its fields, an API operation — and any contributor that can materialize one
|
|
16
|
+
// publishes it here.
|
|
17
|
+
type GraphNodeKind string
|
|
18
|
+
|
|
19
|
+
const (
|
|
20
|
+
// GraphNodeMarkdownDocument is a whole Markdown document.
|
|
21
|
+
GraphNodeMarkdownDocument GraphNodeKind = "markdown_document"
|
|
22
|
+
// GraphNodeMarkdownSection is one heading and the span it opens. It carries
|
|
23
|
+
// the heading's text, never the section's content.
|
|
24
|
+
GraphNodeMarkdownSection GraphNodeKind = "markdown_section"
|
|
25
|
+
// GraphNodePrismaModel is a Prisma model declaration.
|
|
26
|
+
GraphNodePrismaModel GraphNodeKind = "prisma_model"
|
|
27
|
+
// GraphNodePrismaColumn is a scalar field of a Prisma model.
|
|
28
|
+
GraphNodePrismaColumn GraphNodeKind = "prisma_column"
|
|
29
|
+
// GraphNodePrismaRelation is a relation field of a Prisma model. It is its
|
|
30
|
+
// own kind because a relation has two sides and only one usually carries the
|
|
31
|
+
// declaration, which a column never does.
|
|
32
|
+
GraphNodePrismaRelation GraphNodeKind = "prisma_relation"
|
|
33
|
+
// GraphNodeSwaggerOperation is one method-and-path operation of an API
|
|
34
|
+
// document.
|
|
35
|
+
GraphNodeSwaggerOperation GraphNodeKind = "swagger_operation"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// GraphNodeKinds returns every kind a consumer accepts, in declaration order.
|
|
39
|
+
//
|
|
40
|
+
// A consumer seeds its vocabulary from this rather than from a list of its own:
|
|
41
|
+
// a kind added to the block above and not to a consumer's map is a node drawn
|
|
42
|
+
// or ranked as something it is not.
|
|
43
|
+
func GraphNodeKinds() []GraphNodeKind {
|
|
44
|
+
return []GraphNodeKind{
|
|
45
|
+
GraphNodeMarkdownDocument,
|
|
46
|
+
GraphNodeMarkdownSection,
|
|
47
|
+
GraphNodePrismaModel,
|
|
48
|
+
GraphNodePrismaColumn,
|
|
49
|
+
GraphNodePrismaRelation,
|
|
50
|
+
GraphNodeSwaggerOperation,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// GraphNode is one artifact a declaration's documentation can cite.
|
|
55
|
+
//
|
|
56
|
+
// It is a value, not a behavior, for the same reason Hint is: the host
|
|
57
|
+
// serializes the set and hands it to a consumer that reads it long after the
|
|
58
|
+
// lint process exited. What travels is what the consumer can answer from.
|
|
59
|
+
//
|
|
60
|
+
// The node is an index entry, never content. A section carries its heading and
|
|
61
|
+
// where it starts; the text under that heading is read from the file when
|
|
62
|
+
// someone actually needs it, exactly as a function body is.
|
|
63
|
+
type GraphNode struct {
|
|
64
|
+
// Address is the identity a citation names, verbatim — `docs/sale.md#pricing`,
|
|
65
|
+
// `prisma:Sale.price`, `POST:/orders/{orderId}`.
|
|
66
|
+
//
|
|
67
|
+
// The rule that produced it owns the grammar. A consumer keys on the string
|
|
68
|
+
// and parses none of it: the address forms come from a Markdown anchor
|
|
69
|
+
// generator, a Prisma parser, and an OpenAPI normalizer, and re-deriving any
|
|
70
|
+
// of them outside the rule that owns it would be a second implementation of a
|
|
71
|
+
// published contract.
|
|
72
|
+
Address string `json:"address"`
|
|
73
|
+
|
|
74
|
+
// Kind is what this node is. A node whose kind is not in GraphNodeKinds is
|
|
75
|
+
// dropped by the consumer.
|
|
76
|
+
Kind GraphNodeKind `json:"kind"`
|
|
77
|
+
|
|
78
|
+
// Readable is the human-facing name — a heading's own text, a model's name.
|
|
79
|
+
// Empty when the artifact has none beyond its address.
|
|
80
|
+
Readable string `json:"readable,omitempty"`
|
|
81
|
+
|
|
82
|
+
// Parent is the Address of the node containing this one: a section's document
|
|
83
|
+
// or enclosing section, a column's model. Empty at the top of a containment
|
|
84
|
+
// chain. A parent naming no published node is dropped rather than fabricated.
|
|
85
|
+
Parent string `json:"parent,omitempty"`
|
|
86
|
+
|
|
87
|
+
// File is where the artifact lives, as the rule spells it. Empty when the
|
|
88
|
+
// artifact has no file — an API operation is named by method and path, and
|
|
89
|
+
// which document declared it is not part of its identity.
|
|
90
|
+
File string `json:"file,omitempty"`
|
|
91
|
+
|
|
92
|
+
// Line is the 1-based line the node starts on, or 0 when it has no position.
|
|
93
|
+
Line int `json:"line,omitempty"`
|
|
94
|
+
|
|
95
|
+
// Aliases are the additional addresses this same node answers to, when the
|
|
96
|
+
// rule exposes it by more than one path. They resolve to this node rather
|
|
97
|
+
// than to copies of it, so an artifact reachable twice is one node.
|
|
98
|
+
Aliases []string `json:"aliases,omitempty"`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// GraphContext is the read-only handle the host passes to GraphNodes.
|
|
102
|
+
//
|
|
103
|
+
// It mirrors HintContext exactly, and for the same reason: a rule value is
|
|
104
|
+
// stateless, so without State here a projection could only ever return
|
|
105
|
+
// constants.
|
|
106
|
+
type GraphContext struct {
|
|
107
|
+
// Identity names the Program these nodes were built for, as during Check.
|
|
108
|
+
Identity ProjectIdentity
|
|
109
|
+
|
|
110
|
+
// State is the value the rule passed to ProjectContext.SetState.
|
|
111
|
+
State any
|
|
112
|
+
|
|
113
|
+
// Severity and Options are the resolved configuration Check ran under.
|
|
114
|
+
Severity Severity
|
|
115
|
+
Options json.RawMessage
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// DecodeOptions unmarshals the configured options into out. A missing options
|
|
119
|
+
// tuple leaves out unchanged and returns nil.
|
|
120
|
+
func (c *GraphContext) DecodeOptions(out interface{}) error {
|
|
121
|
+
if c == nil || len(c.Options) == 0 {
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
return json.Unmarshal(c.Options, out)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// GraphRule is an optional marker a ProjectRule implements to publish the
|
|
128
|
+
// artifacts a declaration's documentation can cite.
|
|
129
|
+
//
|
|
130
|
+
// The gate is HintRule's, for the same reasons: called at most once per Program,
|
|
131
|
+
// always after Check, only when a consumer asks — never during `ttsc check` —
|
|
132
|
+
// and never unless Check passed and published state. A rule configured off is
|
|
133
|
+
// never asked, so `off` means no nodes with no code in the rule.
|
|
134
|
+
//
|
|
135
|
+
// Pull, not push. A set of artifacts is a projection of FINISHED state; a rule
|
|
136
|
+
// pushing nodes while building that state would publish the ones it had found
|
|
137
|
+
// so far rather than the ones the project has.
|
|
138
|
+
//
|
|
139
|
+
// What crosses this boundary is what an artifact *is*, never what the rule
|
|
140
|
+
// decided about it. No coverage, no cardinality, no policy, no diagnostic:
|
|
141
|
+
// those are the linter's product and it already delivers them as compile
|
|
142
|
+
// errors. A consumer that received them would hold a second, unmaintained
|
|
143
|
+
// answer to a question the linter already answers.
|
|
144
|
+
type GraphRule interface {
|
|
145
|
+
ProjectRule
|
|
146
|
+
|
|
147
|
+
// GraphNodes returns the artifacts this rule materialized, in any order.
|
|
148
|
+
// Containment is expressed by Parent rather than by position.
|
|
149
|
+
GraphNodes(ctx *GraphContext) []GraphNode
|
|
150
|
+
}
|