@ttsc/lint 0.14.0-dev.20260528 → 0.14.0-dev.20260529.1
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 +4 -11
- package/linthost/lsp.go +149 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
[](https://github.com/samchon/ttsc/blob/master/LICENSE)
|
|
6
|
-
[](https://www.npmjs.com/package/@ttsc/lint)
|
|
7
|
-
[](https://www.npmjs.com/package/@ttsc/lint)
|
|
8
|
-
[](https://github.com/samchon/ttsc/actions?query=workflow%3Atest)
|
|
9
|
-
[](https://ttsc.dev/docs)
|
|
10
|
-
[](https://discord.gg/E94XhzrUCZ)
|
|
5
|
+
[](https://github.com/samchon/ttsc/blob/master/LICENSE) [](https://www.npmjs.com/package/@ttsc/lint) [](https://www.npmjs.com/package/@ttsc/lint) [](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [](https://ttsc.dev/docs) [](https://discord.gg/E94XhzrUCZ)
|
|
11
6
|
|
|
12
7
|
A linter and formatter. Co-protagonist of the [`ttsc`](https://ttsc.dev) toolchain — paired with `ttsc`, it replaces `eslint` and `prettier`.
|
|
13
8
|
|
|
@@ -196,7 +191,6 @@ Rules:
|
|
|
196
191
|
joined the family list.
|
|
197
192
|
-->
|
|
198
193
|
|
|
199
|
-
|
|
200
194
|
### ESLint core
|
|
201
195
|
|
|
202
196
|
Generic ESLint-compatible rules that apply to both JavaScript and TypeScript source. Every rule listed here corresponds 1-to-1 with an ESLint core rule of the same kebab-case id, so projects migrating from ESLint can paste their rule severities into `lint.config.ts` without renaming anything. TypeScript-only rules and `@typescript-eslint` extensions live under `typescript/*` in [TypeScript](#typescript) — `@ttsc/lint` does not accept legacy bare names or `@typescript-eslint/*` aliases for those.
|
|
@@ -526,7 +520,7 @@ Source: [`eslint-plugin-jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-j
|
|
|
526
520
|
- [`jsx-a11y/heading-has-content`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-heading-has-content.tsx): rejects empty JSX headings.
|
|
527
521
|
- [`jsx-a11y/html-has-lang`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-html-has-lang.tsx): requires `<html>` JSX elements to declare a non-empty `lang` attribute.
|
|
528
522
|
- [`jsx-a11y/iframe-has-title`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-iframe-has-title.tsx): requires every `<iframe>` JSX element to declare a non-empty, unique `title`.
|
|
529
|
-
- [`jsx-a11y/img-redundant-alt`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-img-redundant-alt.tsx): rejects redundant words such as
|
|
523
|
+
- [`jsx-a11y/img-redundant-alt`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-img-redundant-alt.tsx): rejects redundant words such as _image_, _photo_, or _picture_ inside the `alt` attribute of an `<img>`.
|
|
530
524
|
- [`jsx-a11y/interactive-supports-focus`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-interactive-supports-focus.tsx): requires elements with interactive ARIA roles to be focusable.
|
|
531
525
|
- [`jsx-a11y/label-has-associated-control`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-label-has-associated-control.tsx): requires `<label>` elements to wrap a form control or reference one via `htmlFor`.
|
|
532
526
|
- [`jsx-a11y/label-has-for`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/jsx-a11y-label-has-for.tsx): deprecated predecessor of `label-has-associated-control` that checks the same nesting / `htmlFor` association requirement.
|
|
@@ -991,7 +985,7 @@ Source: [`eslint-plugin-regexp`](https://github.com/ota-meshi/eslint-plugin-rege
|
|
|
991
985
|
|
|
992
986
|
### Security
|
|
993
987
|
|
|
994
|
-
Security-focused TypeScript source rules. Report likely security smells — non-literal sinks for eval, file I/O, regex construction, child-process spawning, cryptographic primitives — that warrant human review even if no exploit is statically provable. Treat findings as
|
|
988
|
+
Security-focused TypeScript source rules. Report likely security smells — non-literal sinks for eval, file I/O, regex construction, child-process spawning, cryptographic primitives — that warrant human review even if no exploit is statically provable. Treat findings as _hints_, not proofs.
|
|
995
989
|
|
|
996
990
|
Source: [`eslint-plugin-security@4.0.0`](https://github.com/eslint-community/eslint-plugin-security).
|
|
997
991
|
|
|
@@ -1080,7 +1074,7 @@ export default {
|
|
|
1080
1074
|
|
|
1081
1075
|
### Architecture boundaries
|
|
1082
1076
|
|
|
1083
|
-
Architecture-boundary rules enforce import direction and module visibility between configured source-path
|
|
1077
|
+
Architecture-boundary rules enforce import direction and module visibility between configured source-path _elements_ (layers, features, apps in a monorepo). Every rule operates on the _resolved source file_ of an import — relative imports are followed to the real `.ts`/`.tsx`/`.d.ts` file before classification. Boundary diagnostics do not offer autofixes — a violation usually needs an API or architecture decision, not a mechanical import rewrite.
|
|
1084
1078
|
|
|
1085
1079
|
Source: ported from [`eslint-plugin-boundaries`](https://github.com/javierbrea/eslint-plugin-boundaries).
|
|
1086
1080
|
|
|
@@ -1204,4 +1198,3 @@ To the maintainers of every plugin listed above: the rule semantics under `react
|
|
|
1204
1198
|
If you would prefer to publish a first-party `@ttsc/lint` plugin for your family yourself, you are welcome to take the Go sources under [`packages/lint/linthost/rules_*.go`](https://github.com/samchon/ttsc/tree/master/packages/lint/linthost) and the fixtures under [`tests/test-lint/src/cases/`](https://github.com/samchon/ttsc/tree/master/tests/test-lint/src/cases) and ship them as your own contributor plugin. Open an issue at [samchon/ttsc](https://github.com/samchon/ttsc/issues) when the upstream package is ready, and I will retire the in-tree port and add a redirect line under [Rules](#rules) pointing at your package. Same offer for partial coverage — name a subset and I will remove just those rules.
|
|
1205
1199
|
|
|
1206
1200
|
The contributor-plugin walkthrough is the [`@ttsc/lint` development guide](https://ttsc.dev/docs/development/walkthroughs/lint).
|
|
1207
|
-
|
package/linthost/lsp.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"errors"
|
|
6
6
|
"flag"
|
|
7
7
|
"fmt"
|
|
8
|
+
"io"
|
|
8
9
|
"io/fs"
|
|
9
10
|
"net/url"
|
|
10
11
|
"os"
|
|
@@ -12,6 +13,10 @@ import (
|
|
|
12
13
|
"strings"
|
|
13
14
|
"unicode/utf16"
|
|
14
15
|
"unicode/utf8"
|
|
16
|
+
|
|
17
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
18
|
+
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
19
|
+
shimparser "github.com/microsoft/typescript-go/shim/parser"
|
|
15
20
|
)
|
|
16
21
|
|
|
17
22
|
const (
|
|
@@ -67,8 +72,13 @@ type lspCommandOptions struct {
|
|
|
67
72
|
argumentsJSON string
|
|
68
73
|
command string
|
|
69
74
|
contextJSON string
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// contentStdin reports whether the caller passed --content-stdin. When
|
|
76
|
+
// set, RunLSPExecuteCommand reads the FULL document buffer from os.Stdin
|
|
77
|
+
// (to EOF) and formats that text in memory instead of reading the target
|
|
78
|
+
// file from disk. See lspFormatBuffer.
|
|
79
|
+
contentStdin bool
|
|
80
|
+
cwd string
|
|
81
|
+
pluginsJSON string
|
|
72
82
|
// rangeJSON is accepted from the ttsc LSP server
|
|
73
83
|
// (`internal/lspserver/lsp_native_plugin_source.go`) for forward
|
|
74
84
|
// compatibility, but no current code path consumes it — code actions
|
|
@@ -180,6 +190,24 @@ func RunLSPExecuteCommand(args []string) int {
|
|
|
180
190
|
return 2
|
|
181
191
|
}
|
|
182
192
|
opts.uri = uri
|
|
193
|
+
// --content-stdin selects the lightweight in-memory format path: the full
|
|
194
|
+
// document buffer is read from stdin and formatted with AST+source rules
|
|
195
|
+
// only, with no temp-workspace copy and no tsgo Program. It applies to
|
|
196
|
+
// ttsc.format.document; ttsc.lint.fixAll under --content-stdin is out of
|
|
197
|
+
// scope (lint-class fixes can require a type checker), so it falls back to
|
|
198
|
+
// the disk-based path below.
|
|
199
|
+
if opts.contentStdin && opts.command == commandFormatDocument {
|
|
200
|
+
content, err := io.ReadAll(os.Stdin)
|
|
201
|
+
if err != nil {
|
|
202
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint lsp-execute-command: read --content-stdin: %v\n", err)
|
|
203
|
+
return 2
|
|
204
|
+
}
|
|
205
|
+
edit, code := lspFormatBuffer(string(content), opts)
|
|
206
|
+
if code != 0 {
|
|
207
|
+
return code
|
|
208
|
+
}
|
|
209
|
+
return writeJSON(edit)
|
|
210
|
+
}
|
|
183
211
|
edit, code := lspWorkspaceEditForCommand(opts)
|
|
184
212
|
if code != 0 {
|
|
185
213
|
return code
|
|
@@ -198,6 +226,7 @@ func parseLSPCommandOptions(name string, args []string) (*lspCommandOptions, boo
|
|
|
198
226
|
contextJSON := fs.String("context-json", "", "")
|
|
199
227
|
command := fs.String("command", "", "")
|
|
200
228
|
argumentsJSON := fs.String("arguments-json", "", "")
|
|
229
|
+
contentStdin := fs.Bool("content-stdin", false, "")
|
|
201
230
|
if err := fs.Parse(args); err != nil {
|
|
202
231
|
return nil, false
|
|
203
232
|
}
|
|
@@ -210,6 +239,7 @@ func parseLSPCommandOptions(name string, args []string) (*lspCommandOptions, boo
|
|
|
210
239
|
argumentsJSON: *argumentsJSON,
|
|
211
240
|
command: *command,
|
|
212
241
|
contextJSON: *contextJSON,
|
|
242
|
+
contentStdin: *contentStdin,
|
|
213
243
|
cwd: resolvedCwd,
|
|
214
244
|
pluginsJSON: *pluginsJSON,
|
|
215
245
|
rangeJSON: *rangeJSON,
|
|
@@ -421,6 +451,123 @@ func lspWorkspaceEditForCommand(opts *lspCommandOptions) (*lspWorkspaceEdit, int
|
|
|
421
451
|
return workspaceEditForFullDocument(opts.uri, string(original), string(next)), 0
|
|
422
452
|
}
|
|
423
453
|
|
|
454
|
+
// lspFormatBuffer formats an in-memory document buffer using only the
|
|
455
|
+
// format-class rules, with no tsgo Program and no temp-workspace copy. It is
|
|
456
|
+
// the lightweight path behind --content-stdin for ttsc.format.document.
|
|
457
|
+
//
|
|
458
|
+
// Format rules are AST+source only (`IsFormat() == true`); none implement
|
|
459
|
+
// typeAwareRule, so the engine runs them with a nil checker. The document
|
|
460
|
+
// content comes entirely from `content` — the file on disk at opts.uri is
|
|
461
|
+
// never read — so an editor can format an unsaved buffer without paying the
|
|
462
|
+
// disk-copy + full-program-load cost of lspWorkspaceEditForCommand.
|
|
463
|
+
//
|
|
464
|
+
// Returns the same WorkspaceEdit shape as the disk path, or (nil, 0) on a
|
|
465
|
+
// no-op (no fixable findings, or text unchanged after convergence).
|
|
466
|
+
func lspFormatBuffer(content string, opts *lspCommandOptions) (*lspWorkspaceEdit, int) {
|
|
467
|
+
target, err := filePathFromURI(opts.uri)
|
|
468
|
+
if err != nil {
|
|
469
|
+
fmt.Fprintln(os.Stderr, err)
|
|
470
|
+
return nil, 2
|
|
471
|
+
}
|
|
472
|
+
// Guard rails mirror lspWorkspaceEditForCommand: skip targets outside the
|
|
473
|
+
// project root or inside node_modules.
|
|
474
|
+
if _, ok := projectRelativePath(opts.cwd, target); !ok {
|
|
475
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: LSP command target %s is outside cwd %s\n", target, opts.cwd)
|
|
476
|
+
return nil, 2
|
|
477
|
+
}
|
|
478
|
+
if projectPathHasSegment(opts.cwd, target, "node_modules") {
|
|
479
|
+
return nil, 0
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
|
|
483
|
+
if err != nil {
|
|
484
|
+
fmt.Fprintln(os.Stderr, err)
|
|
485
|
+
return nil, 2
|
|
486
|
+
}
|
|
487
|
+
rules = formatCommandResolver{inner: rules}
|
|
488
|
+
engine := NewEngineWithResolver(rules)
|
|
489
|
+
if engine.NeedsTypeChecker() {
|
|
490
|
+
// A format-class contributor rule (formatContributorAdapter) needs the type
|
|
491
|
+
// checker, which the single-file in-memory parse can't supply. Fall back to
|
|
492
|
+
// the disk path, which builds a full program with a checker, so the result
|
|
493
|
+
// matches `ttsc format`; the dirty buffer can't be honored for these rules.
|
|
494
|
+
// Built-in format rules are AST-only, so they keep the fast in-memory path.
|
|
495
|
+
return lspWorkspaceEditForCommand(opts)
|
|
496
|
+
}
|
|
497
|
+
scriptKind := scriptKindForPath(target)
|
|
498
|
+
|
|
499
|
+
text := content
|
|
500
|
+
converged := false
|
|
501
|
+
for pass := 0; pass < maxFormatPasses; pass++ {
|
|
502
|
+
file := shimparser.ParseSourceFile(shimast.SourceFileParseOptions{FileName: target}, text, scriptKind)
|
|
503
|
+
if file == nil {
|
|
504
|
+
// Match the disk path: a buffer we can't parse is a benign no-op, not a
|
|
505
|
+
// hard error — don't fight the editor's own diagnostics on a dirty buffer.
|
|
506
|
+
return nil, 0
|
|
507
|
+
}
|
|
508
|
+
findings := filterFormatFindings(engine.Run([]*shimast.SourceFile{file}, nil))
|
|
509
|
+
next, applied := applyFindingFixesToText(text, findings)
|
|
510
|
+
if applied == 0 {
|
|
511
|
+
converged = true
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
text = next
|
|
515
|
+
}
|
|
516
|
+
if !converged {
|
|
517
|
+
fmt.Fprintf(os.Stderr,
|
|
518
|
+
"@ttsc/lint: LSP %s cascade did not converge after %d passes\n",
|
|
519
|
+
opts.command, maxFormatPasses)
|
|
520
|
+
return nil, 2
|
|
521
|
+
}
|
|
522
|
+
return workspaceEditForFullDocument(opts.uri, content, text), 0
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// scriptKindForPath maps a file extension to the tsgo ScriptKind the parser
|
|
526
|
+
// needs so TS/JSX-only syntax is recognized. Mirrors the test helpers'
|
|
527
|
+
// ScriptKind selection (helpers_test.go parseTSFile/parseTSXFile).
|
|
528
|
+
func scriptKindForPath(path string) shimcore.ScriptKind {
|
|
529
|
+
switch strings.ToLower(filepath.Ext(path)) {
|
|
530
|
+
case ".tsx":
|
|
531
|
+
return shimcore.ScriptKindTSX
|
|
532
|
+
case ".jsx":
|
|
533
|
+
return shimcore.ScriptKindJSX
|
|
534
|
+
case ".js", ".cjs", ".mjs":
|
|
535
|
+
return shimcore.ScriptKindJS
|
|
536
|
+
default:
|
|
537
|
+
return shimcore.ScriptKindTS
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// applyFindingFixesToText is the in-memory counterpart of
|
|
542
|
+
// applyFindingFixes/applyTextEditsToFile (fix.go): it collects every fixable
|
|
543
|
+
// finding's TextEdit, selects a non-overlapping set with the same
|
|
544
|
+
// selectTextEdits logic, applies them right-to-left to `text`, and returns the
|
|
545
|
+
// new string plus the number of edits applied. It never writes to disk and
|
|
546
|
+
// never reloads a Program. Findings carry byte offsets into the same `text`
|
|
547
|
+
// that was just parsed, so no per-file grouping is needed.
|
|
548
|
+
func applyFindingFixesToText(text string, findings []*Finding) (string, int) {
|
|
549
|
+
edits := make([]TextEdit, 0, len(findings))
|
|
550
|
+
for _, finding := range findings {
|
|
551
|
+
if finding == nil || len(finding.Fix) == 0 {
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
edits = append(edits, finding.Fix...)
|
|
555
|
+
}
|
|
556
|
+
selected := selectTextEdits(len(text), edits)
|
|
557
|
+
if len(selected) == 0 {
|
|
558
|
+
return text, 0
|
|
559
|
+
}
|
|
560
|
+
next := text
|
|
561
|
+
for i := len(selected) - 1; i >= 0; i-- {
|
|
562
|
+
edit := selected[i]
|
|
563
|
+
next = next[:edit.Pos] + edit.Text + next[edit.End:]
|
|
564
|
+
}
|
|
565
|
+
if next == text {
|
|
566
|
+
return text, 0
|
|
567
|
+
}
|
|
568
|
+
return next, len(selected)
|
|
569
|
+
}
|
|
570
|
+
|
|
424
571
|
func workspaceEditForFullDocument(uri string, original string, next string) *lspWorkspaceEdit {
|
|
425
572
|
if original == next {
|
|
426
573
|
return nil
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/lint",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.20260529.1",
|
|
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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@typescript/native-preview": "7.0.0-dev.20260527.2",
|
|
37
37
|
"@types/node": "^25.3.0",
|
|
38
38
|
"rimraf": "^6.1.2",
|
|
39
|
-
"ttsc": "0.14.0-dev.
|
|
39
|
+
"ttsc": "0.14.0-dev.20260529.1"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|