@ttsc/lint 0.7.1 → 0.7.3

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/README.md CHANGED
@@ -9,7 +9,48 @@
9
9
  [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://github.com/samchon/ttsc/tree/master/docs)
10
10
  [![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ)
11
11
 
12
- `@ttsc/lint` reports ESLint-style diagnostics from the same TypeScript-Go type-check pass that `ttsc` already runs.
12
+ Lint as compile errors.
13
+
14
+ Rules run inside `ttsc`'s `typescript-go` type-check pass — one compile, both checks.
15
+
16
+ ## Demonstration
17
+
18
+ Given this file:
19
+
20
+ ```typescript
21
+ // src/lint.ts
22
+ var x: number = 3;
23
+ let y: number = 4;
24
+ const z: string = 5;
25
+
26
+ console.log(x + y + z);
27
+ ```
28
+
29
+ Run `ttsc` with `@ttsc/lint` enabled (see [Setup](#setup)):
30
+
31
+ ```bash
32
+ $ pnpm ttsc
33
+ src/lint.ts:3:7 - error TS2322: Type 'number' is not assignable to type 'string'.
34
+
35
+ 3 const z: string = 5;
36
+ ~
37
+
38
+ src/lint.ts:2:5 - error TS17397: [prefer-const] Use const instead of let.
39
+
40
+ 2 let y: number = 4;
41
+ ~~~~~~~~~~~~~
42
+
43
+ src/lint.ts:1:1 - error TS11966: [no-var] Unexpected var, use let or const instead.
44
+
45
+ 1 var x: number = 3;
46
+ ~~~~~~~~~~~~~~~~~~
47
+
48
+ Found 3 errors in the same file, starting at: src/lint.ts:3
49
+ ```
50
+
51
+ Type errors (`TS2322`) and lint violations (`TS17397`, `TS11966`) come out together, in the same `error TSxxxxx` shape.
52
+
53
+ CI that already runs `ttsc` blocks on lint with no extra wiring.
13
54
 
14
55
  ## Setup
15
56
 
@@ -19,7 +60,9 @@ Install `ttsc`, TypeScript-Go, and the lint plugin:
19
60
  npm install -D ttsc @typescript/native-preview @ttsc/lint
20
61
  ```
21
62
 
22
- Open your project's `tsconfig.json`, then add this entry under `compilerOptions.plugins`. If the file already has `compilerOptions`, merge this into the existing object and keep `@ttsc/lint` as the first active plugin:
63
+ Add the plugin under `compilerOptions.plugins` in your `tsconfig.json`.
64
+
65
+ Keep `@ttsc/lint` as the first plugin entry — see [Plugin order](#plugin-order).
23
66
 
24
67
  ```jsonc
25
68
  {
@@ -39,16 +82,20 @@ Open your project's `tsconfig.json`, then add this entry under `compilerOptions.
39
82
  }
40
83
  ```
41
84
 
42
- Run your normal `ttsc` or `ttsx` command:
85
+ Then run your normal `ttsc` or `ttsx`:
43
86
 
44
87
  ```bash
45
88
  npx ttsc
46
89
  npx ttsx src/index.ts
47
90
  ```
48
91
 
49
- Lint errors fail the command. With `ttsx`, lint errors stop the program before your entrypoint runs. Lint warnings are printed without changing the exit code.
92
+ - Lint errors fail the command.
93
+ - Under `ttsx`, lint errors stop the program before your entrypoint runs.
94
+ - Lint warnings are printed without changing the exit code.
95
+
96
+ ### External config file
50
97
 
51
- You can also keep the lint rules in a standalone config file and leave only the file reference in `tsconfig.json`:
98
+ You can also keep the rules in a standalone file and reference it from `tsconfig.json`:
52
99
 
53
100
  ```jsonc
54
101
  {
@@ -73,28 +120,17 @@ export default {
73
120
  };
74
121
  ```
75
122
 
76
- `config` accepts either a config object or a path to `.json`, `.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, or `.mts`. JavaScript and TypeScript config files may export the object directly, as `default`, as `config`, or as a function that returns the object. Relative paths are resolved from the owning tsconfig directory.
123
+ The `config` field accepts:
77
124
 
78
- Inline `config` is also accepted:
125
+ - An inline object (shown earlier), **or** a path to a file ending in `.json`, `.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, or `.mts`.
126
+ - For JS/TS configs: a `default` export, a named `config` export, a direct module export, or a function that returns the object.
127
+ - Relative paths resolve from the directory of the owning `tsconfig.json`.
79
128
 
80
- ```jsonc
81
- {
82
- "compilerOptions": {
83
- "plugins": [
84
- {
85
- "transform": "@ttsc/lint",
86
- "config": {
87
- "no-var": "error"
88
- }
89
- }
90
- ]
91
- }
92
- }
93
- ```
129
+ ## Plugin order
94
130
 
95
- ## Notes
131
+ `@ttsc/lint` must be the first plugin in `compilerOptions.plugins`.
96
132
 
97
- `@ttsc/lint` must be the first active plugin entry because it reports on the source code you wrote. Output plugins such as `@ttsc/banner`, `@ttsc/paths`, and `@ttsc/strip` can come after it.
133
+ It inspects the source you wrote, so output plugins (`@ttsc/banner`, `@ttsc/paths`, `@ttsc/strip`) have to come after it.
98
134
 
99
135
  ```jsonc
100
136
  {
@@ -112,7 +148,9 @@ Inline `config` is also accepted:
112
148
  }
113
149
  ```
114
150
 
115
- This package is diagnostic-only today: no autofix, no recommended preset, no custom rule loading, and no cross-file lint rules.
151
+ ## Scope
152
+
153
+ Diagnostic-only today: no autofix, no recommended preset, no custom rule loading, and no cross-file lint rules.
116
154
 
117
155
  ## Rules
118
156
 
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,0DAA6B;AAQ7B,qDAAmC;AAEnC,0BACE,QAA0D;IAE1D,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC;QAC/C,KAAK,EAAE,OAAO;KACf,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,0DAA6B;AAK7B,qDAAmC;AAEnC,0BACE,QAA0D;IAE1D,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC;QAC/C,KAAK,EAAE,OAAO;KACf,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/lint",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Reference ttsc plugin: ESLint-style lint rules hosted in the same Program/Checker as the type-check pass.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -27,10 +27,10 @@
27
27
  "plugin"
28
28
  ],
29
29
  "devDependencies": {
30
- "@typescript/native-preview": "7.0.0-dev.20260430.1",
30
+ "@typescript/native-preview": "7.0.0-dev.20260501.1",
31
31
  "@types/node": "^25.3.0",
32
32
  "rimraf": "^6.1.2",
33
- "ttsc": "0.7.1"
33
+ "ttsc": "0.7.3"
34
34
  },
35
35
  "repository": {
36
36
  "type": "git",
@@ -1,10 +1,10 @@
1
1
  package main
2
2
 
3
3
  import (
4
- "strings"
4
+ "strings"
5
5
 
6
- shimast "github.com/microsoft/typescript-go/shim/ast"
7
- shimscanner "github.com/microsoft/typescript-go/shim/scanner"
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
8
8
  )
9
9
 
10
10
  // nodeText returns the source text under a node with all leading
@@ -14,213 +14,213 @@ import (
14
14
  // would include any preceding comment because tsgo's Pos points at
15
15
  // the start of leading trivia, not the actual token.
16
16
  func nodeText(file *shimast.SourceFile, node *shimast.Node) string {
17
- if file == nil || node == nil {
18
- return ""
19
- }
20
- src := file.Text()
21
- end := node.End()
22
- pos := shimscanner.SkipTrivia(src, node.Pos())
23
- if pos < 0 || end > len(src) || pos >= end {
24
- return ""
25
- }
26
- return strings.TrimRight(src[pos:end], " \t\r\n")
17
+ if file == nil || node == nil {
18
+ return ""
19
+ }
20
+ src := file.Text()
21
+ end := node.End()
22
+ pos := shimscanner.SkipTrivia(src, node.Pos())
23
+ if pos < 0 || end > len(src) || pos >= end {
24
+ return ""
25
+ }
26
+ return strings.TrimRight(src[pos:end], " \t\r\n")
27
27
  }
28
28
 
29
29
  // identifierText returns the lexical name of an Identifier node, or "" if
30
30
  // the node isn't an Identifier.
31
31
  func identifierText(node *shimast.Node) string {
32
- if node == nil || node.Kind != shimast.KindIdentifier {
33
- return ""
34
- }
35
- id := node.AsIdentifier()
36
- if id == nil {
37
- return ""
38
- }
39
- return id.Text
32
+ if node == nil || node.Kind != shimast.KindIdentifier {
33
+ return ""
34
+ }
35
+ id := node.AsIdentifier()
36
+ if id == nil {
37
+ return ""
38
+ }
39
+ return id.Text
40
40
  }
41
41
 
42
42
  // stripParens descends through ParenthesizedExpression nodes and returns
43
43
  // the first non-parenthesized child. ESLint rules typically operate on
44
44
  // the canonical form.
45
45
  func stripParens(node *shimast.Node) *shimast.Node {
46
- for node != nil && node.Kind == shimast.KindParenthesizedExpression {
47
- next := node.AsParenthesizedExpression()
48
- if next == nil || next.Expression == nil {
49
- return node
50
- }
51
- node = next.Expression
52
- }
53
- return node
46
+ for node != nil && node.Kind == shimast.KindParenthesizedExpression {
47
+ next := node.AsParenthesizedExpression()
48
+ if next == nil || next.Expression == nil {
49
+ return node
50
+ }
51
+ node = next.Expression
52
+ }
53
+ return node
54
54
  }
55
55
 
56
56
  // isMatchingPropertyAccess reports whether `node` reads the chain
57
57
  // `head.tail[0].tail[1]…`. Useful for detecting `obj.__proto__` or
58
58
  // `console.log` shapes regardless of nesting.
59
59
  func isMatchingPropertyAccess(node *shimast.Node, head string, tail ...string) bool {
60
- if node == nil || node.Kind != shimast.KindPropertyAccessExpression {
61
- return false
62
- }
63
- chain := []*shimast.Node{}
64
- cur := node
65
- for cur != nil && cur.Kind == shimast.KindPropertyAccessExpression {
66
- access := cur.AsPropertyAccessExpression()
67
- if access == nil {
68
- return false
69
- }
70
- chain = append([]*shimast.Node{access.Name()}, chain...)
71
- cur = access.Expression
72
- }
73
- if cur == nil || identifierText(cur) != head {
74
- return false
75
- }
76
- if len(chain) != len(tail) {
77
- return false
78
- }
79
- for i, want := range tail {
80
- if identifierText(chain[i]) != want {
81
- return false
82
- }
83
- }
84
- return true
60
+ if node == nil || node.Kind != shimast.KindPropertyAccessExpression {
61
+ return false
62
+ }
63
+ chain := []*shimast.Node{}
64
+ cur := node
65
+ for cur != nil && cur.Kind == shimast.KindPropertyAccessExpression {
66
+ access := cur.AsPropertyAccessExpression()
67
+ if access == nil {
68
+ return false
69
+ }
70
+ chain = append([]*shimast.Node{access.Name()}, chain...)
71
+ cur = access.Expression
72
+ }
73
+ if cur == nil || identifierText(cur) != head {
74
+ return false
75
+ }
76
+ if len(chain) != len(tail) {
77
+ return false
78
+ }
79
+ for i, want := range tail {
80
+ if identifierText(chain[i]) != want {
81
+ return false
82
+ }
83
+ }
84
+ return true
85
85
  }
86
86
 
87
87
  // isLiteralBoolean returns the boolean value (and ok=true) for a
88
88
  // `KindTrueKeyword` / `KindFalseKeyword` literal. Other nodes return
89
89
  // (false, false).
90
90
  func isLiteralBoolean(node *shimast.Node) (bool, bool) {
91
- if node == nil {
92
- return false, false
93
- }
94
- switch node.Kind {
95
- case shimast.KindTrueKeyword:
96
- return true, true
97
- case shimast.KindFalseKeyword:
98
- return false, true
99
- }
100
- return false, false
91
+ if node == nil {
92
+ return false, false
93
+ }
94
+ switch node.Kind {
95
+ case shimast.KindTrueKeyword:
96
+ return true, true
97
+ case shimast.KindFalseKeyword:
98
+ return false, true
99
+ }
100
+ return false, false
101
101
  }
102
102
 
103
103
  // isLiteralExpression returns true for nodes whose value is intrinsically
104
104
  // truthy / falsy at parse time — these flag `no-constant-condition` etc.
105
105
  func isLiteralExpression(node *shimast.Node) bool {
106
- if node == nil {
107
- return false
108
- }
109
- switch node.Kind {
110
- case
111
- shimast.KindStringLiteral,
112
- shimast.KindNumericLiteral,
113
- shimast.KindBigIntLiteral,
114
- shimast.KindNoSubstitutionTemplateLiteral,
115
- shimast.KindRegularExpressionLiteral,
116
- shimast.KindTrueKeyword,
117
- shimast.KindFalseKeyword,
118
- shimast.KindNullKeyword:
119
- return true
120
- }
121
- return false
106
+ if node == nil {
107
+ return false
108
+ }
109
+ switch node.Kind {
110
+ case
111
+ shimast.KindStringLiteral,
112
+ shimast.KindNumericLiteral,
113
+ shimast.KindBigIntLiteral,
114
+ shimast.KindNoSubstitutionTemplateLiteral,
115
+ shimast.KindRegularExpressionLiteral,
116
+ shimast.KindTrueKeyword,
117
+ shimast.KindFalseKeyword,
118
+ shimast.KindNullKeyword:
119
+ return true
120
+ }
121
+ return false
122
122
  }
123
123
 
124
124
  // callCalleeName returns the simple-identifier callee of a CallExpression
125
125
  // (e.g. `eval` from `eval("...")`). Returns "" when the callee is more
126
126
  // complex than a bare identifier.
127
127
  func callCalleeName(call *shimast.CallExpression) string {
128
- if call == nil || call.Expression == nil {
129
- return ""
130
- }
131
- return identifierText(call.Expression)
128
+ if call == nil || call.Expression == nil {
129
+ return ""
130
+ }
131
+ return identifierText(call.Expression)
132
132
  }
133
133
 
134
134
  // numericLiteralText returns the literal text of a numeric / bigint
135
135
  // literal, normalized for the comparisons rules need (`-0`, `0xFF`).
136
136
  func numericLiteralText(node *shimast.Node) string {
137
- if node == nil {
138
- return ""
139
- }
140
- switch node.Kind {
141
- case shimast.KindNumericLiteral:
142
- if lit := node.AsNumericLiteral(); lit != nil {
143
- return lit.Text
144
- }
145
- case shimast.KindBigIntLiteral:
146
- if lit := node.AsBigIntLiteral(); lit != nil {
147
- return lit.Text
148
- }
149
- }
150
- return ""
137
+ if node == nil {
138
+ return ""
139
+ }
140
+ switch node.Kind {
141
+ case shimast.KindNumericLiteral:
142
+ if lit := node.AsNumericLiteral(); lit != nil {
143
+ return lit.Text
144
+ }
145
+ case shimast.KindBigIntLiteral:
146
+ if lit := node.AsBigIntLiteral(); lit != nil {
147
+ return lit.Text
148
+ }
149
+ }
150
+ return ""
151
151
  }
152
152
 
153
153
  // stringLiteralText returns the value of a string-shaped literal:
154
154
  // StringLiteral or NoSubstitutionTemplateLiteral.
155
155
  func stringLiteralText(node *shimast.Node) string {
156
- if node == nil {
157
- return ""
158
- }
159
- switch node.Kind {
160
- case shimast.KindStringLiteral:
161
- if lit := node.AsStringLiteral(); lit != nil {
162
- return lit.Text
163
- }
164
- case shimast.KindNoSubstitutionTemplateLiteral:
165
- if lit := node.AsNoSubstitutionTemplateLiteral(); lit != nil {
166
- return lit.Text
167
- }
168
- }
169
- return ""
156
+ if node == nil {
157
+ return ""
158
+ }
159
+ switch node.Kind {
160
+ case shimast.KindStringLiteral:
161
+ if lit := node.AsStringLiteral(); lit != nil {
162
+ return lit.Text
163
+ }
164
+ case shimast.KindNoSubstitutionTemplateLiteral:
165
+ if lit := node.AsNoSubstitutionTemplateLiteral(); lit != nil {
166
+ return lit.Text
167
+ }
168
+ }
169
+ return ""
170
170
  }
171
171
 
172
172
  // walkDescendants visits node and every child below it, depth-first.
173
173
  func walkDescendants(node *shimast.Node, visit func(*shimast.Node)) {
174
- if node == nil {
175
- return
176
- }
177
- visit(node)
178
- node.ForEachChild(func(child *shimast.Node) bool {
179
- walkDescendants(child, visit)
180
- return false
181
- })
174
+ if node == nil {
175
+ return
176
+ }
177
+ visit(node)
178
+ node.ForEachChild(func(child *shimast.Node) bool {
179
+ walkDescendants(child, visit)
180
+ return false
181
+ })
182
182
  }
183
183
 
184
184
  func bindingIdentifierNames(node *shimast.Node) []string {
185
- if node == nil {
186
- return nil
187
- }
188
- if name := identifierText(node); name != "" {
189
- return []string{name}
190
- }
191
- if node.Kind != shimast.KindObjectBindingPattern && node.Kind != shimast.KindArrayBindingPattern {
192
- return nil
193
- }
194
- var names []string
195
- walkDescendants(node, func(child *shimast.Node) {
196
- if child == node {
197
- return
198
- }
199
- if name := identifierText(child); name != "" {
200
- names = append(names, name)
201
- }
202
- })
203
- return names
185
+ if node == nil {
186
+ return nil
187
+ }
188
+ if name := identifierText(node); name != "" {
189
+ return []string{name}
190
+ }
191
+ if node.Kind != shimast.KindObjectBindingPattern && node.Kind != shimast.KindArrayBindingPattern {
192
+ return nil
193
+ }
194
+ var names []string
195
+ walkDescendants(node, func(child *shimast.Node) {
196
+ if child == node {
197
+ return
198
+ }
199
+ if name := identifierText(child); name != "" {
200
+ names = append(names, name)
201
+ }
202
+ })
203
+ return names
204
204
  }
205
205
 
206
206
  func isLiteralLike(node *shimast.Node) bool {
207
- node = stripParens(node)
208
- if node == nil {
209
- return false
210
- }
211
- if isLiteralExpression(node) {
212
- return true
213
- }
214
- if node.Kind == shimast.KindPrefixUnaryExpression {
215
- prefix := node.AsPrefixUnaryExpression()
216
- if prefix == nil {
217
- return false
218
- }
219
- switch prefix.Operator {
220
- case shimast.KindPlusToken, shimast.KindMinusToken:
221
- return prefix.Operand != nil &&
222
- (prefix.Operand.Kind == shimast.KindNumericLiteral || prefix.Operand.Kind == shimast.KindBigIntLiteral)
223
- }
224
- }
225
- return false
207
+ node = stripParens(node)
208
+ if node == nil {
209
+ return false
210
+ }
211
+ if isLiteralExpression(node) {
212
+ return true
213
+ }
214
+ if node.Kind == shimast.KindPrefixUnaryExpression {
215
+ prefix := node.AsPrefixUnaryExpression()
216
+ if prefix == nil {
217
+ return false
218
+ }
219
+ switch prefix.Operator {
220
+ case shimast.KindPlusToken, shimast.KindMinusToken:
221
+ return prefix.Operand != nil &&
222
+ (prefix.Operand.Kind == shimast.KindNumericLiteral || prefix.Operand.Kind == shimast.KindBigIntLiteral)
223
+ }
224
+ }
225
+ return false
226
226
  }