@ttsc/lint 0.14.0-dev.20260528.1 → 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 CHANGED
@@ -2,12 +2,7 @@
2
2
 
3
3
  ![banner of @ttsc/lint](https://ttsc.dev/og.jpg)
4
4
 
5
- [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE)
6
- [![NPM Version](https://img.shields.io/npm/v/@ttsc/lint.svg)](https://www.npmjs.com/package/@ttsc/lint)
7
- [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/lint.svg)](https://www.npmjs.com/package/@ttsc/lint)
8
- [![Build Status](https://github.com/samchon/ttsc/workflows/test/badge.svg)](https://github.com/samchon/ttsc/actions?query=workflow%3Atest)
9
- [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://ttsc.dev/docs)
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)
5
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE) [![NPM Version](https://img.shields.io/npm/v/@ttsc/lint.svg)](https://www.npmjs.com/package/@ttsc/lint) [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/lint.svg)](https://www.npmjs.com/package/@ttsc/lint) [![Build Status](https://github.com/samchon/ttsc/workflows/test/badge.svg)](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://ttsc.dev/docs) [![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
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 *image*, *photo*, or *picture* inside the `alt` attribute of an `<img>`.
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 *hints*, not proofs.
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 *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.
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
- cwd string
71
- pluginsJSON string
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.20260528.1",
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.20260528.1"
39
+ "ttsc": "0.14.0-dev.20260529.1"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",