Skip to content

Commit c802ca0

Browse files
authored
feat: add support for typescript AST to getStaticValue, and ReferenceTracker (#255)
1 parent 92ccc4b commit c802ca0

9 files changed

+325
-30
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,19 @@ jobs:
9696
with:
9797
node-version: ${{ matrix.node }}
9898

99-
- name: 📥 Install dependencies
100-
run: npm install
101-
99+
# If we run this after `npm install`, it may hang with npm warnings.
102100
- name: 📥 Install ESLint v${{ matrix.eslint }}
103101
run: npm install --save-dev eslint@${{ matrix.eslint }}
104102

103+
- name: 📥 Install dependencies
104+
run: npm install
105+
105106
- name: 🏗 Build
106107
run: npm run build
107108

108109
- name: ▶️ Run test script
109110
run: |
110-
npm run test
111+
npm run test-coverage
111112
112113
# Load src/index.mjs to check for syntax errors.
113114
# This is because for some reason the exit code does not become 1 in Node.js 12.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ Please use GitHub's Issues/PRs.
3030

3131
### Development Tools
3232

33-
- `npm test` runs tests and measures coverage.
34-
- `npm run clean` removes the coverage result of `npm test` command.
35-
- `npm run coverage` shows the coverage result of the last `npm test` command.
33+
- `npm run test-coverage` runs tests and measures coverage.
34+
- `npm run clean` removes the coverage result of `npm run test-coverage` command.
35+
- `npm run coverage` shows the coverage result of the last `npm run test-coverage` command.
3636
- `npm run lint` runs ESLint.
3737
- `npm run watch` runs tests on each file change.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@
4545
"lint:installed-check": "installed-check -v -i installed-check -i npm-run-all2 -i knip -i rollup-plugin-dts",
4646
"lint:knip": "knip",
4747
"lint": "run-p lint:*",
48-
"test": "c8 mocha --reporter dot \"test/*.mjs\"",
49-
"preversion": "npm test && npm run -s build",
48+
"test-coverage": "c8 mocha --reporter dot \"test/*.mjs\"",
49+
"test": "mocha --reporter dot \"test/*.mjs\"",
50+
"preversion": "npm test-coverage && npm run -s build",
5051
"postversion": "git push && git push --tags",
5152
"prewatch": "npm run -s clean",
5253
"watch": "warun \"{src,test}/**/*.mjs\" -- npm run -s test:mocha"
@@ -58,6 +59,8 @@
5859
"@eslint-community/eslint-plugin-mysticatea": "^15.6.1",
5960
"@types/eslint": "^9.6.1",
6061
"@types/estree": "^1.0.7",
62+
"@typescript-eslint/parser": "^5.62.0",
63+
"@typescript-eslint/types": "^5.62.0",
6164
"c8": "^8.0.1",
6265
"dot-prop": "^7.2.0",
6366
"eslint": "^8.57.1",

src/get-static-value.mjs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { findVariable } from "./find-variable.mjs"
44
/** @typedef {import("./types.mjs").StaticValue} StaticValue */
55
/** @typedef {import("eslint").Scope.Scope} Scope */
66
/** @typedef {import("estree").Node} Node */
7-
/** @typedef {import("eslint").Rule.Node} RuleNode */
8-
/** @typedef {import("eslint").Rule.NodeTypes} NodeTypes */
9-
/** @typedef {import("estree").MemberExpression} MemberExpression */
10-
/** @typedef {import("estree").Property} Property */
11-
/** @typedef {import("estree").RegExpLiteral} RegExpLiteral */
12-
/** @typedef {import("estree").BigIntLiteral} BigIntLiteral */
13-
/** @typedef {import("estree").SimpleLiteral} SimpleLiteral */
7+
/** @typedef {import("@typescript-eslint/types").TSESTree.Node} TSESTreeNode */
8+
/** @typedef {import("@typescript-eslint/types").TSESTree.AST_NODE_TYPES} TSESTreeNodeTypes */
9+
/** @typedef {import("@typescript-eslint/types").TSESTree.MemberExpression} MemberExpression */
10+
/** @typedef {import("@typescript-eslint/types").TSESTree.Property} Property */
11+
/** @typedef {import("@typescript-eslint/types").TSESTree.RegExpLiteral} RegExpLiteral */
12+
/** @typedef {import("@typescript-eslint/types").TSESTree.BigIntLiteral} BigIntLiteral */
13+
/** @typedef {import("@typescript-eslint/types").TSESTree.Literal} Literal */
1414

1515
const globalObject =
1616
typeof globalThis !== "undefined"
@@ -236,7 +236,7 @@ function isGetter(object, name) {
236236

237237
/**
238238
* Get the element values of a given node list.
239-
* @param {(Node|null)[]} nodeList The node list to get values.
239+
* @param {(Node|TSESTreeNode|null)[]} nodeList The node list to get values.
240240
* @param {Scope|undefined|null} initialScope The initial scope to find variables.
241241
* @returns {any[]|null} The value list if all nodes are constant. Otherwise, null.
242242
*/
@@ -284,14 +284,14 @@ function isEffectivelyConst(variable) {
284284
}
285285

286286
/**
287-
* @template {NodeTypes} T
287+
* @template {TSESTreeNodeTypes} T
288288
* @callback VisitorCallback
289-
* @param {RuleNode & { type: T }} node
289+
* @param {TSESTreeNode & { type: T }} node
290290
* @param {Scope|undefined|null} initialScope
291291
* @returns {StaticValue | null}
292292
*/
293293
/**
294-
* @typedef { { [K in NodeTypes]?: VisitorCallback<K> } } Operations
294+
* @typedef { { [K in TSESTreeNodeTypes]?: VisitorCallback<K> } } Operations
295295
*/
296296
/**
297297
* @type {Operations}
@@ -542,7 +542,7 @@ const operations = Object.freeze({
542542

543543
Literal(node) {
544544
const literal =
545-
/** @type {Partial<SimpleLiteral> & Partial<RegExpLiteral> & Partial<BigIntLiteral>} */ (
545+
/** @type {Partial<Literal> & Partial<RegExpLiteral> & Partial<BigIntLiteral>} */ (
546546
node
547547
)
548548
//istanbul ignore if : this is implementation-specific behavior.
@@ -749,18 +749,33 @@ const operations = Object.freeze({
749749

750750
return null
751751
},
752+
TSAsExpression(node, initialScope) {
753+
return getStaticValueR(node.expression, initialScope)
754+
},
755+
TSSatisfiesExpression(node, initialScope) {
756+
return getStaticValueR(node.expression, initialScope)
757+
},
758+
TSTypeAssertion(node, initialScope) {
759+
return getStaticValueR(node.expression, initialScope)
760+
},
761+
TSNonNullExpression(node, initialScope) {
762+
return getStaticValueR(node.expression, initialScope)
763+
},
764+
TSInstantiationExpression(node, initialScope) {
765+
return getStaticValueR(node.expression, initialScope)
766+
},
752767
})
753768

754769
/**
755770
* Get the value of a given node if it's a static value.
756-
* @param {Node|null|undefined} node The node to get.
771+
* @param {Node|TSESTreeNode|null|undefined} node The node to get.
757772
* @param {Scope|undefined|null} initialScope The scope to start finding variable.
758773
* @returns {StaticValue|null} The static value of the node, or `null`.
759774
*/
760775
function getStaticValueR(node, initialScope) {
761776
if (node != null && Object.hasOwnProperty.call(operations, node.type)) {
762777
return /** @type {VisitorCallback<any>} */ (operations[node.type])(
763-
/** @type {RuleNode} */ (node),
778+
/** @type {TSESTreeNode} */ (node),
764779
initialScope,
765780
)
766781
}

src/reference-tracker.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getStringIfConstant } from "./get-string-if-constant.mjs"
2121
/** @typedef {import("estree").Property} Property */
2222
/** @typedef {import("estree").AssignmentProperty} AssignmentProperty */
2323
/** @typedef {import("estree").Literal} Literal */
24+
/** @typedef {import("@typescript-eslint/types").TSESTree.Node} TSESTreeNode */
2425
/** @typedef {import("./types.mjs").ReferenceTrackerOptions} ReferenceTrackerOptions */
2526
/**
2627
* @template T
@@ -82,7 +83,7 @@ function isModifiedGlobal(variable) {
8283
* @returns {node is RuleNode & {parent: Expression}} `true` if the node is passed through.
8384
*/
8485
function isPassThrough(node) {
85-
const parent = /** @type {RuleNode} */ (node).parent
86+
const parent = /** @type {TSESTreeNode} */ (node).parent
8687

8788
if (parent) {
8889
switch (parent.type) {
@@ -96,6 +97,12 @@ function isPassThrough(node) {
9697
)
9798
case "ChainExpression":
9899
return true
100+
case "TSAsExpression":
101+
case "TSSatisfiesExpression":
102+
case "TSTypeAssertion":
103+
case "TSNonNullExpression":
104+
case "TSInstantiationExpression":
105+
return true
99106

100107
default:
101108
return false

test/get-static-value.mjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import tsParser from "@typescript-eslint/parser"
12
import assert from "assert"
23
import eslint from "eslint"
34
import semver from "semver"
45
import { getStaticValue } from "../src/index.mjs"
56
import { getScope, newCompatLinter } from "./test-lib/eslint-compat.mjs"
67

78
describe("The 'getStaticValue' function", () => {
8-
for (const { code, expected, noScope = false } of [
9+
for (const { code, expected, noScope = false, parser } of [
910
{ code: "[]", expected: { value: [] } },
1011
{ code: "[1, 2, 3]", expected: { value: [1, 2, 3] } },
1112
{ code: "[,, 3]", expected: { value: [, , 3] } }, //eslint-disable-line no-sparse-arrays
@@ -397,6 +398,32 @@ const aMap = Object.freeze({
397398
},
398399
]
399400
: []),
401+
// TypeScript support
402+
{
403+
code: `const a = 42; a as number;`,
404+
expected: { value: 42 },
405+
parser: tsParser,
406+
},
407+
{
408+
code: `const a = 42; a satisfies number;`,
409+
expected: { value: 42 },
410+
parser: tsParser,
411+
},
412+
{
413+
code: `const a = 42; <number>a;`,
414+
expected: { value: 42 },
415+
parser: tsParser,
416+
},
417+
{
418+
code: `const a = 42; a!;`,
419+
expected: { value: 42 },
420+
parser: tsParser,
421+
},
422+
{
423+
code: `const a = 42; a<number>;`,
424+
expected: { value: 42 },
425+
parser: tsParser,
426+
},
400427
]) {
401428
it(`should return ${JSON.stringify(expected)} from ${code}`, () => {
402429
const linter = newCompatLinter()
@@ -407,6 +434,7 @@ const aMap = Object.freeze({
407434
ecmaVersion: semver.gte(eslint.Linter.version, "8.0.0")
408435
? 2022
409436
: 2020,
437+
parser,
410438
},
411439
rules: { "test/test": "error" },
412440
plugins: {

test/has-side-effect.mjs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import tsParser from "@typescript-eslint/parser"
12
import assert from "assert"
23
import { getProperty } from "dot-prop"
34
import eslint from "eslint"
@@ -6,7 +7,13 @@ import { hasSideEffect } from "../src/index.mjs"
67
import { newCompatLinter } from "./test-lib/eslint-compat.mjs"
78

89
describe("The 'hasSideEffect' function", () => {
9-
for (const { code, key = "body[0].expression", options, expected } of [
10+
for (const {
11+
code,
12+
key = "body[0].expression",
13+
options,
14+
expected,
15+
parser,
16+
} of [
1017
{
1118
code: "777",
1219
options: undefined,
@@ -300,6 +307,32 @@ describe("The 'hasSideEffect' function", () => {
300307
options: undefined,
301308
expected: false,
302309
},
310+
// TypeScript support
311+
{
312+
code: `a as number`,
313+
expected: false,
314+
parser: tsParser,
315+
},
316+
{
317+
code: `a satisfies number`,
318+
expected: false,
319+
parser: tsParser,
320+
},
321+
{
322+
code: `<number>a`,
323+
expected: false,
324+
parser: tsParser,
325+
},
326+
{
327+
code: `a!`,
328+
expected: false,
329+
parser: tsParser,
330+
},
331+
{
332+
code: `a<number>`,
333+
expected: false,
334+
parser: tsParser,
335+
},
303336
]) {
304337
it(`should return ${expected} on the code \`${code}\` and the options \`${JSON.stringify(
305338
options,
@@ -312,6 +345,7 @@ describe("The 'hasSideEffect' function", () => {
312345
ecmaVersion: semver.gte(eslint.Linter.version, "8.0.0")
313346
? 2022
314347
: 2020,
348+
parser,
315349
},
316350
rules: { "test/test": "error" },
317351
plugins: {

0 commit comments

Comments
 (0)