diff --git a/.depcheckrc b/.depcheckrc new file mode 100644 index 0000000..664390d --- /dev/null +++ b/.depcheckrc @@ -0,0 +1,36 @@ +# Copyright IBM Corp. 2025 +# Assisted by CursorAI + +# Ignore these dependencies that are used but not detected by depcheck +ignores: + # VSCode API is imported as 'vscode' but the package is '@types/vscode' + - "vscode" + # React dependencies used in webview libs (external bundles, not npm dependencies) + - "react" + - "@graphiql/react" + # Test runner used by vscode-test command but not directly imported + - "@vscode/test-electron" + # Depcheck itself is used via npx in scripts + - "depcheck" + +# Ignore these patterns - webview libs are bundled separately +ignorePatterns: + - "webview/**" + - "dist/**" + - "out/**" + - "*.vsix" + +# Skip missing dependencies check for known false positives +skipMissing: false + +# Custom parsers for different file types +parsers: + "**/*.ts": "typescript" + "**/*.js": "es6" + "**/*.mjs": "es6" + +# Detectors to use +detectors: + - "requireCallExpression" + - "importDeclaration" + - "exportDeclaration" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a9f4cf1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,43 @@ +# Copyright IBM Corp. 2025 +# Assisted by CursorAI + +name: Lint + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Check for unused exports with ts-prune + run: npm run lint:prune + + - name: Check for unused dependencies with depcheck + run: npm run lint:deps + + - name: Run type checking + run: npm run check-types diff --git a/package-lock.json b/package-lock.json index 2135343..ac0cb91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "20.x", + "@types/sinon": "^17.0.3", "@types/vscode": "^1.100.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", @@ -21,7 +22,9 @@ "depcheck": "^1.4.7", "esbuild": "^0.25.3", "eslint": "^9.25.1", + "mocha": "^10.8.2", "npm-run-all": "^4.1.5", + "sinon": "^19.0.2", "ts-prune": "^0.10.3", "typescript": "^5.8.3" }, @@ -969,6 +972,55 @@ "node": ">=14" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@ts-morph/common": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", @@ -1058,6 +1110,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.100.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", @@ -4219,6 +4288,13 @@ "setimmediate": "^1.0.5" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4299,6 +4375,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4725,6 +4809,20 @@ "dev": true, "license": "MIT" }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5314,6 +5412,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -5966,6 +6074,48 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", + "integrity": "sha512-r15s9/s+ub/d4bxNXqIUmwp6imVSdTorIRaxoecYjqTVLZ8RuoXr/4EDGwIBo6Waxn7f2gnURX9zuhAfCwaF6Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6422,6 +6572,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", diff --git a/package.json b/package.json index a4d02a0..c84d1de 100644 --- a/package.json +++ b/package.json @@ -125,11 +125,15 @@ "check-types": "tsc --noEmit", "lint": "eslint src", "lint:prune": "ts-prune --error", + "lint:deps": "depcheck", + "lint:all": "npm run lint && npm run lint:prune && npm run lint:deps", + "ci:lint": "npm run lint && npm run lint:prune && npm run lint:deps && npm run check-types", "test": "vscode-test" }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "20.x", + "@types/sinon": "^17.0.3", "@types/vscode": "^1.100.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", @@ -138,7 +142,9 @@ "depcheck": "^1.4.7", "esbuild": "^0.25.3", "eslint": "^9.25.1", + "mocha": "^10.8.2", "npm-run-all": "^4.1.5", + "sinon": "^19.0.2", "ts-prune": "^0.10.3", "typescript": "^5.8.3" }, diff --git a/readme.md b/readme.md index a63ff45..2981d2f 100644 --- a/readme.md +++ b/readme.md @@ -135,6 +135,38 @@ npm run compile npm run watch ``` +### Code Quality & Linting + +This project uses multiple linting tools to ensure code quality: + +```bash +# Run ESLint for code style and best practices +npm run lint + +# Check for unused TypeScript exports +npm run lint:prune + +# Check for unused dependencies +npm run lint:deps + +# Run type checking +npm run check-types + +# Run all linting tools (used in CI) +npm run ci:lint +``` + +**CI Integration**: All linting tools run automatically in GitHub Actions on every push and pull request. The CI will fail if any of these tools report issues: + +- **ESLint** - Enforces code style, prevents console statements, ensures proper imports +- **ts-prune** - Finds unused exports to keep the codebase clean +- **depcheck** - Identifies unused dependencies and missing dependencies + +Configuration files: +- ESLint: `eslint.config.mjs` +- depcheck: `.depcheckrc` +- TypeScript: `tsconfig.json` + ### Testing The extension uses the standard VS Code testing framework with Mocha. The tests are located in the `src/test` directory. View the README.md file in that directory for more details. diff --git a/src/extension.ts b/src/extension.ts index 23794f5..1c203a7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,18 +19,6 @@ export let EXTENSION_URI: vscode.Uri; export let runtimeDiag: vscode.DiagnosticCollection; -/** - * Gets or creates a terminal for StepZen operations - * @param name The name to give the terminal if creating a new one - * @returns An existing or newly created terminal instance - */ -export function getOrCreateStepZenTerminal(name: string = UI.TERMINAL_NAME): vscode.Terminal { - if (!stepzenTerminal) { - stepzenTerminal = vscode.window.createTerminal(name); - } - return stepzenTerminal; -} - /** * Resolve the workspace folder that owns the given URI (or the active editor if none supplied). */ diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c7ce0af..0151aa2 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -98,48 +98,18 @@ export const GRAPHQL = { OPERATION_TYPE_PATTERN: /(query|mutation|subscription)\s+(\w+)/g, } as const; -// Error codes -export const ERROR_CODES = { - VALIDATION_ERROR: "VALIDATION_ERROR", - VALIDATION_FAILED: "VALIDATION_FAILED", - CLI_ERROR: "CLI_ERROR", - NETWORK_ERROR: "NETWORK_ERROR", - STRING_ERROR: "STRING_ERROR", - UNKNOWN_ERROR: "UNKNOWN_ERROR", - INVALID_QUERY: "INVALID_QUERY", -} as const; -// Log levels (enum values) -export const LOG_LEVELS = { - ERROR: "error", - WARN: "warn", - INFO: "info", - DEBUG: "debug", -} as const; // URLs and external links export const URLS = { STEPZEN_CLI_INSTALL: "https://stepzen.com/docs/stepzen-cli/install", } as const; -// Menu groups -export const MENU_GROUPS = { - STEPZEN_EXPLORER: "0_stepzen@1", - STEPZEN_EDITOR_1: "0_stepzen@1", - STEPZEN_EDITOR_2: "0_stepzen@2", - STEPZEN_EDITOR_3: "0_stepzen@3", -} as const; - // Language IDs export const LANGUAGE_IDS = { GRAPHQL: "graphql", } as const; -// Context when conditions -export const WHEN_CONDITIONS = { - GRAPHQL_EDITOR_FOCUS: "editorLangId == graphql && editorTextFocus", -} as const; - // Progress messages export const PROGRESS_MESSAGES = { UPLOADING_SCHEMA: "Uploading schema to StepZen...", @@ -165,10 +135,6 @@ export const MESSAGES = { } as const; // Type definitions for better type safety -export type CommandId = typeof COMMANDS[keyof typeof COMMANDS]; -export type ConfigKey = typeof CONFIG_KEYS[keyof typeof CONFIG_KEYS]; -export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES]; -export type LogLevel = typeof LOG_LEVELS[keyof typeof LOG_LEVELS]; export type GraphQLOperationType = typeof GRAPHQL.ROOT_OPERATION_TYPES[number]; export type GraphQLScalarType = typeof GRAPHQL.SCALAR_TYPES[number]; diff --git a/src/utils/stepzenProjectScanner.ts b/src/utils/stepzenProjectScanner.ts index 9643ef0..4a29983 100644 --- a/src/utils/stepzenProjectScanner.ts +++ b/src/utils/stepzenProjectScanner.ts @@ -673,7 +673,7 @@ function unwrapType(type: TypeNode): string { * @param type The GraphQL type node to analyze * @returns The full type string with appropriate wrapping (e.g., "String!", "[Int]", "[User!]!") */ -export function getFullType(type: TypeNode): string { +function getFullType(type: TypeNode): string { if (!type) { logger.warn('Null type provided to getFullType'); return '';