├── .eslintrc.json ├── .github └── workflows │ ├── deploy.yml │ └── main.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── demo.gif └── icon.png ├── package-lock.json ├── package.json ├── src ├── completionProvider.ts ├── extension.ts ├── python.ts ├── settings.ts ├── typeHintProvider.ts ├── typeSearch.ts ├── typingHintProvider.ts ├── utils.ts └── workspaceSearcher.ts ├── test ├── common.ts ├── index.ts ├── runTest.ts └── suite │ ├── extension.test.ts │ ├── providers │ ├── completionProvider.test.ts │ ├── typeHintProvider.test.ts │ └── typingHintProvider.test.ts │ ├── typeSearch │ ├── classWithSameName.test.ts │ ├── detection.test.ts │ ├── findImport.test.ts │ ├── hintOfSimilarParam.test.ts │ └── invalidTernaryOperator.test.ts │ └── workspaceSearcher.test.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 12, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "error", 14 | "@typescript-eslint/indent": ["error", 4], 15 | "curly": "warn", 16 | "eqeqeq": "warn", 17 | "no-unused-vars": "warn", 18 | "no-throw-literal": "warn", 19 | "no-trailing-spaces":"warn" 20 | }, 21 | "overrides": [ 22 | { 23 | "files": ["**/*.js"], 24 | "rules": { 25 | "@typescript-eslint/semi": "warn", 26 | "@typescript-eslint/indent": "none" 27 | } 28 | }, 29 | { 30 | "files": ["**/*.ts"], 31 | "rules": { 32 | "@typescript-eslint/naming-convention": ["warn", { 33 | "selector": "enum", 34 | "format": null, 35 | "custom": { 36 | "regex": "^[A-Z]\\w*$", 37 | "match": true 38 | } 39 | }] 40 | } 41 | }] 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | release: 4 | types: 5 | - published 6 | workflow_dispatch: 7 | inputs: 8 | publishMS: 9 | description: "Publish to the Microsoft Marketplace" 10 | type: boolean 11 | required: true 12 | default: true 13 | publishOVSX: 14 | description: "Publish to Open VSX" 15 | type: boolean 16 | required: true 17 | default: true 18 | publishGH: 19 | description: "Publish to GitHub Releases" 20 | type: boolean 21 | required: true 22 | default: true 23 | 24 | jobs: 25 | package: 26 | name: Package 27 | runs-on: ubuntu-latest 28 | outputs: 29 | packageName: ${{ steps.setup.outputs.packageName }} 30 | tag: ${{ steps.setup-tag.outputs.tag }} 31 | version: ${{ steps.setup-tag.outputs.version }} 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v2 35 | with: 36 | node-version: 14 37 | registry-url: https://registry.npmjs.org/ 38 | 39 | - name: Install dependencies 40 | run: npm i 41 | 42 | - name: Setup package path 43 | id: setup 44 | run: echo "::set-output name=packageName::$(node -e "console.log(require('./package.json').name + '-' + require('./package.json').version + '.vsix')")" 45 | 46 | - name: Package 47 | run: | 48 | npx vsce package --out ${{ steps.setup.outputs.packageName }} 49 | 50 | - uses: actions/upload-artifact@v2 51 | with: 52 | name: ${{ steps.setup.outputs.packageName }} 53 | path: ./${{ steps.setup.outputs.packageName }} 54 | if-no-files-found: error 55 | 56 | - name: Setup tag 57 | id: setup-tag 58 | run: | 59 | $version = (Get-Content ./package.json -Raw | ConvertFrom-Json).version 60 | Write-Host "tag: release/$version" 61 | Write-Host "::set-output name=tag::release/$version" 62 | Write-Host "::set-output name=version::$version" 63 | shell: pwsh 64 | 65 | publishMS: 66 | name: Publish to VS marketplace 67 | runs-on: ubuntu-latest 68 | needs: package 69 | if: github.event.inputs.publishMS == 'true' 70 | steps: 71 | - uses: actions/checkout@v2 72 | - uses: actions/download-artifact@v2 73 | with: 74 | name: ${{ needs.package.outputs.packageName }} 75 | - name: Publish to VS marketplace 76 | run: npx vsce publish --packagePath ./${{ needs.package.outputs.packageName }} -p ${{ secrets.VSCE_PAT }} 77 | 78 | publishOVSX: 79 | name: Publish to Open VSX 80 | runs-on: ubuntu-latest 81 | needs: package 82 | if: github.event.inputs.publishOVSX == 'true' 83 | steps: 84 | - uses: actions/checkout@v2 85 | - uses: actions/download-artifact@v2 86 | with: 87 | name: ${{ needs.package.outputs.packageName }} 88 | - name: Publish to Open VSX 89 | run: npx ovsx publish ./${{ needs.package.outputs.packageName }} -p ${{ secrets.OVSX_PAT }} 90 | 91 | publishGH: 92 | name: Publish to GitHub releases 93 | runs-on: ubuntu-latest 94 | needs: package 95 | if: github.event.inputs.publishGH == 'true' 96 | steps: 97 | - uses: actions/download-artifact@v2 98 | with: 99 | name: ${{ needs.package.outputs.packageName }} 100 | 101 | - name: Create Release 102 | id: create-release 103 | uses: actions/create-release@v1 104 | env: 105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | with: 107 | tag_name: ${{ needs.package.outputs.tag }} 108 | release_name: Release ${{ needs.package.outputs.version }} 109 | draft: false 110 | prerelease: false 111 | 112 | - name: Upload assets to a Release 113 | uses: AButler/upload-release-assets@v2.0 114 | with: 115 | files: ${{ needs.package.outputs.packageName }} 116 | release-tag: ${{ needs.package.outputs.tag }} 117 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS with Webpack 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | types: [opened, reopened, edited] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [macos-latest, ubuntu-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 20.x 24 | - run: npm install 25 | - run: xvfb-run -a npm test 26 | if: runner.os == 'Linux' 27 | - run: npm test 28 | if: runner.os != 'Linux' 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | out 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Run Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--disable-extensions", 12 | "--extensionDevelopmentPath=${workspaceFolder}" 13 | ], 14 | "outFiles": ["${workspaceFolder}/**/*.js"], 15 | "preLaunchTask": "npm: test-compile" 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "${workspaceFolder}/testworkspace", 24 | "--disable-extensions", 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/index" 27 | ], 28 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 29 | "preLaunchTask": "npm: test-compile" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // do not hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // exclude "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "[typescript]": { 12 | "editor.rulers": [ 13 | 120 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | test/** 5 | out/** 6 | .gitignore 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | 13 | # Webpack related 14 | node_modules 15 | webpack.config.json 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## [1.5.1] - 2022-12-12 5 | 6 | * Prevents the extension from being activated for non-Python languages. 7 | 8 | * Fixes type hints being provided for some function definitions ending with typing module hints, i.e. '-> Dict[str, str]:'. 9 | 10 | ## [1.4.1] - 2020-10-18 11 | 12 | * Added a known issue to the readme. 13 | 14 | ## [1.4.0] - 2020-10-18 15 | 16 | * Added type hints for bytes 17 | * Changed the default workspace search limit to 10 18 | 19 | ## [1.3.0] - 2020-08-22 20 | 21 | * Type hints for the typing module are now provided at the bottom of the list by default 22 | * Fixed the first type hint not always being pre-selected 23 | * Fixed incorrectly sorted type hints in some cases 24 | 25 | ## [1.2.0] - 2020-05-05 26 | 27 | ### Added: 28 | 29 | * Return type hints for the typing module 30 | * Support for non-ASCII class names 31 | 32 | ### Fixed: 33 | 34 | * Default parameter values sometimes being provided within type hint items 35 | 36 | ## [1.1.2] - 2020-04-22 37 | 38 | * Fixed type hints not being provided for async functions. 39 | 40 | ## [1.1.0] - 2020-04-19 41 | 42 | ### Improved: 43 | 44 | * New setting for disabling workspace searching. 45 | * More default hints are provided for the typing module. 46 | * Overall speed. 47 | 48 | ### Fixed: 49 | 50 | * Setting the workspace search limit to 0 not working. 51 | 52 | ## [1.0.2] - 2020-04-16 53 | 54 | * Fixed type hints being provided for non-parameters. 55 | 56 | ## [1.0.1] - 2020-04-13 57 | 58 | * Fixed type hints sometimes being provided when typing ':' within a string. 59 | 60 | ## [1.0.0] - 2020-04-09 61 | 62 | * Initial release -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome. For instance bug fixes, increased test coverage and new features. 4 | 5 | 6 | ## Pull Requests 7 | 8 | * Open pull requests to the dev branch, not master. 9 | * Run the unit tests to ensure no existing functionality has been affected. 10 | * If applicable, write new unit tests to test your changes. 11 | * If applicable, update [README.md](https://github.com/njqdev/vscode-python-typehint/blob/master/README.md) to reflect your changes. 12 | 13 | 14 | ## Contributor License Agreement 15 | 16 | All contributions are subject to the following DCO. 17 | 18 | ``` 19 | Developer Certificate of Origin 20 | Version 1.1 21 | 22 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 23 | 1 Letterman Drive 24 | Suite D4700 25 | San Francisco, CA, 94129 26 | 27 | Everyone is permitted to copy and distribute verbatim copies of this 28 | license document, but changing it is not allowed. 29 | 30 | 31 | Developer's Certificate of Origin 1.1 32 | 33 | By making a contribution to this project, I certify that: 34 | 35 | (a) The contribution was created in whole or in part by me and I 36 | have the right to submit it under the open source license 37 | indicated in the file; or 38 | 39 | (b) The contribution is based upon previous work that, to the best 40 | of my knowledge, is covered under an appropriate open source 41 | license and I have the right under that license to submit that 42 | work with modifications, whether created in whole or in part 43 | by me, under the same open source license (unless I am 44 | permitted to submit under a different license), as indicated 45 | in the file; or 46 | 47 | (c) The contribution was provided directly to me by some other 48 | person who certified (a), (b) or (c) and I have not modified 49 | it. 50 | 51 | (d) I understand and agree that this project and the contribution 52 | are public and that a record of the contribution (including all 53 | personal information I submit with it, including my sign-off) is 54 | maintained indefinitely and may be redistributed consistent with 55 | this project or the open source license(s) involved. 56 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 njqdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Type Hint 2 | 3 | Provides type hint auto-completion for Python, with completion items for built-in types, classes and the typing module. 4 | 5 | 6 | ![](images/demo.gif) 7 | 8 | 9 | ## Features 10 | 11 | * Provides type hint completion items for built-in types, estimated types and the typing module. 12 | 13 | * Estimates the correct type to provide as a completion item. 14 | 15 | * Can search Python files in the workspace for type estimation purposes. 16 | 17 | ## Settings 18 | 19 | | Name | Description | Default 20 | |---|---|---| 21 | | workspace.searchEnabled | _(boolean)_ If enabled, other files in the workspace are searched when estimating types for a parameter. Disabling this will increase performance, but may reduce estimation accuracy. | true 22 | | workspace.searchLimit | _(number)_ The maximum number of files searched in a workspace search. Has no effect if workspace searching is disabled. | 10 23 | 24 | ## Known Issues 25 | 26 | * If workspace searching is enabled, a VSCode event (onDidOpen) is triggered when a file is searched. This causes other extensions that are listening to the event to analyse the same files, which can add the problems of those files to the Problems window. The only way to prevent this, for now, is by disabling the workspace search setting. 27 | 28 | * The difference between function and class constructor calls when detecting types is determined by the first letter being upper case (unless the class or function is defined in the currently edited document). 29 | 30 | ## Installation 31 | 32 | The extension can found on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=njqdev.vscode-python-typehint). 33 | 34 | ------------------------------------------------------------------------------------------- 35 | 36 | 37 |

38 | 39 | Installs 40 | 41 | 42 | CodeFactor 43 | 44 |

45 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njqdev/vscode-python-typehint/957a6fb20c47ae38afa77c26caaf9189c97c38aa/images/demo.gif -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njqdev/vscode-python-typehint/957a6fb20c47ae38afa77c26caaf9189c97c38aa/images/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-python-typehint", 3 | "displayName": "Python Type Hint", 4 | "version": "1.5.1", 5 | "publisher": "njqdev", 6 | "description": "Type hint completion for Python.", 7 | "icon": "images/icon.png", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/njqdev/vscode-python-typehint" 11 | }, 12 | "license": "MIT", 13 | "categories": [ 14 | "Programming Languages" 15 | ], 16 | "keywords": [ 17 | "Python", 18 | "typehint", 19 | "completion", 20 | "autocompletion", 21 | "parameter" 22 | ], 23 | "engines": { 24 | "vscode": "^1.81.0" 25 | }, 26 | "activationEvents": [ 27 | "onLanguage:python" 28 | ], 29 | "main": "./dist/extension", 30 | "contributes": { 31 | "commands": [], 32 | "languages": [ 33 | { 34 | "id": "python", 35 | "aliases": [ 36 | "Python" 37 | ], 38 | "extensions": [ 39 | ".py" 40 | ] 41 | } 42 | ], 43 | "configuration": { 44 | "title": "Python Type Hint", 45 | "properties": { 46 | "workspace.searchEnabled": { 47 | "type": "boolean", 48 | "default": true, 49 | "description": "If enabled, other files in the workspace are searched when estimating types for a parameter. Disabling this will increase performance, but may reduce estimation accuracy." 50 | }, 51 | "workspace.searchLimit": { 52 | "type": "number", 53 | "default": 10, 54 | "description": "The maximum number of files searched in a workspace search. Has no effect if workspace searching is disabled." 55 | } 56 | } 57 | } 58 | }, 59 | "scripts": { 60 | "package": "vsce package", 61 | "publish": "vsce publish", 62 | "vscode:prepublish": "webpack --mode production", 63 | "lint": "eslint --fix --ext .ts .", 64 | "compile": "webpack --mode none", 65 | "watch": "webpack --mode none --watch", 66 | "test-compile": "tsc -p ./", 67 | "test": "npm run test-compile && node ./out/test/runTest.js" 68 | }, 69 | "devDependencies": { 70 | "@types/glob": "^8.1.0", 71 | "@types/mocha": "^10.0.1", 72 | "@types/node": "^20.5.9", 73 | "@types/vscode": "^1.81.0", 74 | "@typescript-eslint/eslint-plugin": "^6.5.0", 75 | "@typescript-eslint/parser": "^6.5.0", 76 | "@vscode/test-electron": "^2.4.1", 77 | "eslint": "^8.48.0", 78 | "glob": "^10.3.4", 79 | "mocha": "^10.7.3", 80 | "ts-loader": "^9.4.4", 81 | "tslint": "^6.1.3", 82 | "typescript": "^5.2.2", 83 | "webpack": "^5.95.0", 84 | "webpack-cli": "^5.1.4" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/completionProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | CompletionContext, 4 | CompletionList, 5 | CompletionItem, 6 | CompletionItemKind, 7 | CompletionItemProvider, 8 | Position, 9 | TextLine, 10 | TextDocument, 11 | Range 12 | } from "vscode"; 13 | import { TypeHintProvider } from "./typeHintProvider"; 14 | import { paramHintTrigger, returnHintTrigger, PythonType, getDataTypeContainer } from "./python"; 15 | import { TypeHintSettings } from "./settings"; 16 | import { WorkspaceSearcher } from "./workspaceSearcher"; 17 | import { TypingHintProvider } from "./typingHintProvider"; 18 | 19 | 20 | export abstract class CompletionProvider { 21 | 22 | protected itemSortPrefix: number = 90; 23 | 24 | /** 25 | * Push type hints to the end of an array of completion items. 26 | */ 27 | protected pushHintsToItems(typeHints: string[], completionItems: CompletionItem[], firstItemSelected: boolean) { 28 | const sortTextPrefix = this.itemSortPrefix.toString(); 29 | completionItems.push( 30 | firstItemSelected 31 | ? this.selectedCompletionItem(typeHints[0]) 32 | : this.newCompletionitem(typeHints[0], sortTextPrefix) 33 | ); 34 | 35 | for (let i = 1; i < typeHints.length; i++) { 36 | completionItems.push(this.newCompletionitem(typeHints[i], sortTextPrefix)); 37 | } 38 | } 39 | 40 | private newCompletionitem = (hint: string, sortTextPrefix: string): CompletionItem => { 41 | const item = new CompletionItem(this.labelFor(hint), CompletionItemKind.TypeParameter); 42 | item.sortText = sortTextPrefix + hint; 43 | return item; 44 | }; 45 | 46 | protected selectedCompletionItem(typeHint: string, sortTextPrefix: string = "0b"): CompletionItem { 47 | let item = new CompletionItem(this.labelFor(typeHint), CompletionItemKind.TypeParameter); 48 | item.sortText = `${sortTextPrefix}${typeHint}`; 49 | item.preselect = true; 50 | return item; 51 | } 52 | 53 | protected labelFor(typeHint: string): string { 54 | return " " + typeHint; 55 | } 56 | 57 | abstract provideCompletionItems( 58 | doc: TextDocument, 59 | pos: Position, 60 | token: CancellationToken, 61 | context: CompletionContext 62 | ): Promise; 63 | } 64 | 65 | /** 66 | * Provides one or more parameter type hint {@link CompletionItem}. 67 | */ 68 | export class ParamHintCompletionProvider extends CompletionProvider implements CompletionItemProvider { 69 | 70 | private settings: TypeHintSettings; 71 | 72 | constructor(settings: TypeHintSettings) { 73 | super(); 74 | this.settings = settings; 75 | } 76 | 77 | public async provideCompletionItems( 78 | doc: TextDocument, 79 | pos: Position, 80 | token: CancellationToken, 81 | context: CompletionContext 82 | ): Promise { 83 | if (context.triggerCharacter === paramHintTrigger) { 84 | const items: CompletionItem[] = []; 85 | const line = doc.lineAt(pos); 86 | const precedingText = line.text.substring(0, pos.character - 1).trim(); 87 | 88 | if (this.shouldProvideItems(precedingText, pos, doc) && !token.isCancellationRequested) { 89 | const param = this.getParam(precedingText); 90 | const documentText = doc.getText(); 91 | const typeContainer = getDataTypeContainer(); 92 | const provider = new TypeHintProvider(typeContainer); 93 | const wsSearcher = new WorkspaceSearcher(doc.uri, this.settings, typeContainer); 94 | let estimations: string[] = []; 95 | 96 | if (param && !token.isCancellationRequested) { 97 | const workspaceHintSearch = this.settings.workspaceSearchEnabled 98 | ? this.workspaceHintSearch(param, wsSearcher, documentText) 99 | : null; 100 | try { 101 | estimations = await provider.estimateTypeHints(param, documentText); 102 | if (estimations.length > 0) { 103 | this.pushEstimationsToItems(estimations, items); 104 | wsSearcher.cancel(); 105 | } 106 | } catch { 107 | } 108 | 109 | if (token.isCancellationRequested) { 110 | wsSearcher.cancel(); 111 | return Promise.resolve(null); 112 | } 113 | this.pushHintsToItems(provider.remainingTypeHints(), items, estimations.length === 0); 114 | this.itemSortPrefix++; 115 | this.pushHintsToItems(provider.remainingTypingHints(), items, false); 116 | 117 | const hint = await workspaceHintSearch; 118 | if (hint && provider.hintNotProvided(hint)) { 119 | items.unshift(this.selectedCompletionItem(hint, "0a")); 120 | } 121 | return Promise.resolve(new CompletionList(items, false)); 122 | } 123 | } 124 | } 125 | return Promise.resolve(null); 126 | } 127 | 128 | private async workspaceHintSearch(param: string, ws: WorkspaceSearcher, docText: string): Promise { 129 | try { 130 | return ws.findHintOfSimilarParam(param, docText); 131 | } catch { 132 | return null; 133 | } 134 | } 135 | 136 | /** 137 | * Returns the parameter which is about to be type hinted. 138 | * 139 | * @param precedingText The text before the active position. 140 | */ 141 | private getParam(precedingText: string): string | null { 142 | const split = precedingText.split(/[,(*]/); 143 | let param = split.length > 1 ? split[split.length - 1].trim() : precedingText; 144 | return !param || /[!:\]\[?/\\{}.+/=)'";@&£%¤|<>$^~¨ -]/.test(param) ? null : param; 145 | } 146 | 147 | private pushEstimationsToItems(typeHints: string[], items: CompletionItem[]) { 148 | 149 | if (typeHints.length > 0) { 150 | items.push(this.selectedCompletionItem(typeHints[0])); 151 | 152 | for (let i = 1; i < typeHints.length; i++) { 153 | let item = new CompletionItem(this.labelFor(typeHints[i]), CompletionItemKind.TypeParameter); 154 | item.sortText = `${i}${typeHints[i]}`; 155 | items.push(item); 156 | } 157 | } 158 | } 159 | 160 | private shouldProvideItems(precedingText: string, activePos: Position, doc: TextDocument): boolean { 161 | 162 | if (activePos.character > 0 && !/#/.test(precedingText)) { 163 | let provide = /^[ \t]*(def |async *def )/.test(precedingText); 164 | 165 | if (!provide) { 166 | const nLinesToCheck = activePos.line > 4 ? 4 : activePos.line; 167 | const previousLines = doc.getText( 168 | new Range(doc.lineAt(activePos.line - nLinesToCheck).range.start, activePos) 169 | ); 170 | provide = new RegExp(`^[ \t]*(async *)?def(?![\\s\\S]+(\\):|-> *[^:\\s]+:))`, "m").test(previousLines); 171 | } 172 | return provide; 173 | } 174 | return false; 175 | } 176 | } 177 | 178 | /** 179 | * Provides one or more return type hint {@link CompletionItem}. 180 | */ 181 | export class ReturnHintCompletionProvider extends CompletionProvider implements CompletionItemProvider { 182 | 183 | public async provideCompletionItems( 184 | doc: TextDocument, 185 | pos: Position, 186 | token: CancellationToken, 187 | context: CompletionContext 188 | ): Promise { 189 | if (context.triggerCharacter !== returnHintTrigger) { 190 | return null; 191 | } 192 | const items: CompletionItem[] = []; 193 | const line = doc.lineAt(pos); 194 | 195 | if (this.shouldProvideItems(line, pos)) { 196 | const provider = new TypingHintProvider(getDataTypeContainer()); 197 | const detectTypingImport = provider.detectTypingImport(doc.getText()); 198 | 199 | this.pushHintsToItems(Object.values(PythonType), items, true); 200 | this.itemSortPrefix++; 201 | 202 | await detectTypingImport; 203 | this.pushHintsToItems(provider.getRemainingHints(), items, false); 204 | } 205 | return Promise.resolve(new CompletionList(items, false)); 206 | } 207 | 208 | private shouldProvideItems(line: TextLine, pos: Position): boolean { 209 | 210 | if (pos.character > 0 && line.text.substr(pos.character - 2, 2) === "->") { 211 | return /\) *->[: ]*$/m.test(line.text); 212 | } 213 | return false; 214 | } 215 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ParamHintCompletionProvider, ReturnHintCompletionProvider } from './completionProvider'; 3 | import { paramHintTrigger, returnHintTrigger } from "./python"; 4 | import { TypeHintSettings } from './settings'; 5 | 6 | // Called when the extension is activated. 7 | export function activate(context: vscode.ExtensionContext) { 8 | 9 | const settings = new TypeHintSettings(); 10 | 11 | context.subscriptions.push( 12 | vscode.languages.registerCompletionItemProvider( 13 | 'python', 14 | new ParamHintCompletionProvider(settings), 15 | paramHintTrigger 16 | ), 17 | vscode.languages.registerCompletionItemProvider( 18 | 'python', 19 | new ReturnHintCompletionProvider(), 20 | returnHintTrigger 21 | ) 22 | ); 23 | } 24 | 25 | // Called when the extension is deactivated. 26 | export function deactivate() {} 27 | -------------------------------------------------------------------------------- /src/python.ts: -------------------------------------------------------------------------------- 1 | 2 | export const moduleName: string = "[a-zA-Z_][a-zA-Z0-9_.]*"; 3 | 4 | export const paramHintTrigger: string = ":"; 5 | export const returnHintTrigger: string = ">"; 6 | 7 | /** 8 | * A built-in Python type. 9 | */ 10 | export class DataType { 11 | name: PythonType; 12 | category: TypeCategory; 13 | 14 | constructor(name: PythonType, category: TypeCategory) { 15 | this.name = name; 16 | this.category = category; 17 | } 18 | } 19 | 20 | /** 21 | * Container with type name keys and data type values. 22 | */ 23 | export interface DataTypeContainer { 24 | [key: string]: DataType 25 | }; 26 | 27 | export const getDataTypeContainer = (): DataTypeContainer => { 28 | return { 29 | bool: new DataType(PythonType.Bool, typeCategories.bool), 30 | bytes: new DataType(PythonType.Bytes, typeCategories.bytes), 31 | complex: new DataType(PythonType.Complex, typeCategories.complex), 32 | dict: new DataType(PythonType.Dict, typeCategories.dict), 33 | float: new DataType(PythonType.Float, typeCategories.float), 34 | int: new DataType(PythonType.Int, typeCategories.int), 35 | list: new DataType(PythonType.List, typeCategories.list), 36 | object: new DataType(PythonType.Object, typeCategories.object), 37 | set: new DataType(PythonType.Set, typeCategories.set), 38 | str: new DataType(PythonType.String, typeCategories.string), 39 | tuple: new DataType(PythonType.Tuple, typeCategories.tuple) 40 | }; 41 | }; 42 | 43 | /** 44 | * Names of built-in Python types. 45 | */ 46 | export enum PythonType { 47 | Bool = "bool", 48 | Bytes = "bytes", 49 | Complex = "complex", 50 | Dict = "dict", 51 | Float = "float", 52 | Int = "int", 53 | List = "list", 54 | Object = "object", 55 | Set = "set", 56 | String = "str", 57 | Tuple = "tuple", 58 | } 59 | 60 | /** 61 | * Categories of Python types. 62 | */ 63 | export enum TypeCategory { 64 | Abstract, 65 | Basic, 66 | Collection 67 | } 68 | 69 | /** 70 | * Built-in Python type keys with Type category values. 71 | */ 72 | const typeCategories: { [key: string]: TypeCategory } = { 73 | bool: TypeCategory.Basic, 74 | bytes: TypeCategory.Basic, 75 | complex: TypeCategory.Basic, 76 | dict: TypeCategory.Collection, 77 | float: TypeCategory.Basic, 78 | int: TypeCategory.Basic, 79 | list: TypeCategory.Collection, 80 | object: TypeCategory.Abstract, 81 | set: TypeCategory.Collection, 82 | string: TypeCategory.Basic, 83 | tuple: TypeCategory.Collection 84 | }; 85 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { workspace, Event, EventEmitter } from "vscode"; 2 | 3 | /** 4 | * Container of user settings. 5 | */ 6 | export class TypeHintSettings { 7 | 8 | private _workspaceSearchEnabled = true; 9 | private _workspaceSearchLimit = 10; 10 | 11 | constructor() { 12 | workspace.onDidChangeConfiguration(() => { 13 | this.initialize(); 14 | this.settingsUpdated.fire(); 15 | }); 16 | this.initialize(); 17 | } 18 | 19 | public get workspaceSearchLimit() { 20 | return this._workspaceSearchLimit; 21 | } 22 | public get workspaceSearchEnabled() { 23 | return this._workspaceSearchEnabled; 24 | } 25 | 26 | public readonly settingsUpdated = new EventEmitter(); 27 | 28 | public get onDidChangeConfiguration(): Event { 29 | return this.settingsUpdated.event; 30 | } 31 | 32 | private initialize() { 33 | const wsEnable: boolean | undefined = workspace.getConfiguration('workspace').get('searchEnabled'); 34 | const searchLimit: number | undefined = workspace.getConfiguration('workspace').get('searchLimit'); 35 | if (wsEnable !== undefined) { 36 | this._workspaceSearchEnabled = wsEnable; 37 | } 38 | if (searchLimit !== undefined) { 39 | this._workspaceSearchLimit = Number.isInteger(searchLimit) ? searchLimit : Math.round(searchLimit); 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/typeHintProvider.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode"; 2 | import { PythonType as PythonType, DataTypeContainer } from "./python"; 3 | import { TypeSearch, VariableSearchResult } from "./typeSearch"; 4 | import { TypingHintProvider } from "./typingHintProvider"; 5 | import { TypeHintSettings } from "./settings"; 6 | 7 | /** 8 | * Provides type hints. 9 | */ 10 | export class TypeHintProvider { 11 | 12 | private _typingHintProvider: TypingHintProvider; 13 | 14 | private likelyTypes: PythonType[] = [ 15 | PythonType.List, 16 | PythonType.Dict, 17 | PythonType.String, 18 | PythonType.Bool, 19 | PythonType.Int, 20 | PythonType.Tuple, 21 | PythonType.Float 22 | ]; 23 | private _providedTypeHints: string[] = []; 24 | 25 | constructor(typeContainer: DataTypeContainer) { 26 | this._typingHintProvider = new TypingHintProvider(typeContainer); 27 | } 28 | 29 | /** 30 | * Estimates a parameter's type and returns type hints for it. 31 | * The returned hints are ordered with the most likely type being first. 32 | * 33 | * @param param The parameter name. 34 | * @param documentText The text to search in order to estimate types. 35 | */ 36 | public async estimateTypeHints(param: string, documentText: string): Promise { 37 | const typeHints: string[] = []; 38 | 39 | const variableSearch = TypeSearch.variableWithSameName(param, documentText); 40 | 41 | const isTypingImported = this._typingHintProvider.detectTypingImport(documentText); 42 | let typeName = this.getTypeParamEndsWith(param, "_"); 43 | if (typeName) { 44 | this.add(typeName, typeHints); 45 | this.tryAddTypingHint(typeName, typeHints, isTypingImported); 46 | } 47 | 48 | const typesFound = typeHints.length > 0 49 | || this.tryAdd(TypeSearch.hintOfSimilarParam(param, documentText), typeHints) 50 | || this.tryAdd(TypeSearch.classWithSameName(param, documentText), typeHints); 51 | if (typesFound) { 52 | return typeHints; 53 | } 54 | 55 | const searchResult = await variableSearch; 56 | 57 | if (searchResult !== null && !TypeSearch.invalidTernaryOperator(searchResult)) { 58 | this.add(searchResult.typeName, typeHints); 59 | this.tryAddTypingHints(searchResult, typeHints, isTypingImported); 60 | this.tryAdd(this.typeGuessFor(param), typeHints); 61 | } else { 62 | typeName = this.getTypeParamEndsWith(param, ""); 63 | if (typeName) { 64 | this.add(typeName, typeHints); 65 | this.tryAddTypingHint(typeName, typeHints, isTypingImported); 66 | } else { 67 | this.tryAdd(this.typeGuessFor(param), typeHints); 68 | } 69 | } 70 | return typeHints; 71 | } 72 | 73 | /** 74 | * Returns hints for types that have not been provided yet. 75 | */ 76 | public remainingTypeHints(): PythonType[] { 77 | return Object.values(PythonType).filter(type => this.hintNotProvided(type)); 78 | } 79 | 80 | /** 81 | * Returns hints for the typing module that have not been provided yet. 82 | */ 83 | public remainingTypingHints(): string[] { 84 | return this._typingHintProvider.getRemainingHints(); 85 | } 86 | 87 | public hintNotProvided(typeHint: string): boolean { 88 | return !this._providedTypeHints.includes(typeHint); 89 | } 90 | 91 | /** 92 | * If the param ends with a type name, the type is returned. 93 | * @param param The parameter name. 94 | * @param prefix A prefix before the typename. For example, a param named {prefix}list will return 'list'. 95 | */ 96 | private getTypeParamEndsWith(param: string, prefix: string): PythonType | null { 97 | const paramUpper = param.toUpperCase(); 98 | 99 | for (const type of this.likelyTypes) { 100 | if (paramUpper.endsWith(`${prefix}${type.toUpperCase()}`)) { 101 | return type; 102 | } 103 | } 104 | return null; 105 | } 106 | 107 | private typeGuessFor(param: string): string | null { 108 | const typeGuesses: { [key: string]: string } = { 109 | "string": PythonType.String, 110 | "text": PythonType.String, 111 | "path": PythonType.String, 112 | "url": PythonType.String, 113 | "uri": PythonType.String, 114 | "fullpath": PythonType.String, 115 | "full_path": PythonType.String, 116 | "number": PythonType.Int, 117 | "num": PythonType.Int 118 | }; 119 | if (param in typeGuesses) { 120 | return typeGuesses[param]; 121 | } 122 | return null; 123 | } 124 | 125 | 126 | private add(type: string, typeHints: string[]) { 127 | type = type.trim(); 128 | if (this.hintNotProvided(type)) { 129 | typeHints.push(type); 130 | this._providedTypeHints.push(type); 131 | } 132 | } 133 | 134 | private tryAdd(type: string | null, typeHints: string[]): boolean { 135 | if (type) { 136 | this.add(type, typeHints); 137 | return true; 138 | } 139 | return false; 140 | } 141 | 142 | private tryAddTypingHints(searchResult: VariableSearchResult | null, typeHints: string[], typingImported: boolean) { 143 | if (typingImported) { 144 | const hints: string[] | null = this._typingHintProvider.getHints(searchResult); 145 | if (hints) { 146 | for (const hint of hints) { 147 | this.add(hint, typeHints); 148 | } 149 | } 150 | } 151 | } 152 | 153 | private tryAddTypingHint(typeName: string, typeHints: string[], typingImported: boolean) { 154 | if (typingImported) { 155 | const typingHint = this._typingHintProvider.getHint(typeName); 156 | if (typingHint) { 157 | this.add(typingHint, typeHints); 158 | } 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /src/typeSearch.ts: -------------------------------------------------------------------------------- 1 | import { moduleName, PythonType } from "./python"; 2 | 3 | /** 4 | * The source of a type estimation. 5 | */ 6 | export enum EstimationSource { 7 | ClassDefinition, 8 | FunctionDefinition, 9 | Value, 10 | ValueOfOtherVariable, 11 | } 12 | 13 | /** 14 | * The result of a variable type search. 15 | */ 16 | export class VariableSearchResult { 17 | public typeName: string; 18 | public estimationSource: EstimationSource; 19 | public valueAssignment: string; 20 | 21 | constructor(typeName: string, estimationSource: EstimationSource, valueAssignment: string) { 22 | this.typeName = typeName; 23 | this.estimationSource = estimationSource; 24 | this.valueAssignment = valueAssignment; 25 | } 26 | 27 | } 28 | 29 | export class TypeSearch { 30 | 31 | /** 32 | * Searches for a class with the same name as a value and returns the name if found. 33 | * 34 | * @param value The value. 35 | * @param src The source code to search. 36 | */ 37 | public static classWithSameName(value: string, src: string): string | null { 38 | const clsMatch = new RegExp(`^[ \t]*class +(${value})[(:]`, "mi").exec(src); 39 | return clsMatch ? clsMatch[1] : null; 40 | } 41 | 42 | /** 43 | * Searches for a variable with the same name as the param and detects its type. 44 | * 45 | * @param param The parameter name. 46 | * @param src The source code to search. 47 | */ 48 | public static async variableWithSameName(param: string, src: string): Promise { 49 | let variableMatch = this.variableSearchRegExp(param).exec(src); 50 | if (!variableMatch) { 51 | return null; 52 | } 53 | const valueAssignment = variableMatch[1]; 54 | 55 | let typeName = this.detectType(valueAssignment); 56 | if (typeName) { 57 | return new VariableSearchResult(typeName, EstimationSource.Value, valueAssignment); 58 | } 59 | 60 | variableMatch = /^ *([^(\s#"']+)\(?/.exec(valueAssignment); 61 | if (!variableMatch) { 62 | return null; 63 | } 64 | 65 | if (variableMatch[0].endsWith("(")) { 66 | let value = variableMatch[1]; 67 | if (this.classWithSameName(value, src)) { 68 | return new VariableSearchResult(value, EstimationSource.ClassDefinition, valueAssignment); 69 | } 70 | 71 | if (this.isProbablyAClass(value)) { 72 | if (!new RegExp(`^[ \t]*def ${value}\\(` ).test(src)) { 73 | return new VariableSearchResult(value, EstimationSource.Value, valueAssignment); 74 | } 75 | } else { 76 | return this.searchForHintedFunctionCall(value, src, valueAssignment); 77 | } 78 | return null; 79 | } 80 | 81 | // Searching the import source document is not supported 82 | if (!this.isImported(variableMatch[1], src.substr(variableMatch.index - variableMatch.length))) { 83 | variableMatch = this.variableSearchRegExp(variableMatch[1]).exec(src); 84 | if (variableMatch) { 85 | const otherType = this.detectType(variableMatch[1]); 86 | return otherType 87 | ? new VariableSearchResult(otherType, EstimationSource.ValueOfOtherVariable, valueAssignment) 88 | : null; 89 | } 90 | } 91 | return null; 92 | } 93 | 94 | private static searchForHintedFunctionCall( 95 | value: string, 96 | src: string, 97 | valueAssignment: string 98 | ): VariableSearchResult | null { 99 | if (value.includes(".")) { 100 | let split = value.split("."); 101 | value = split[split.length - 1]; 102 | } 103 | 104 | const regExp = new RegExp(`^[ \t]*def ${value}\\([^)]*\\) *-> *([a-zA-Z_][a-zA-Z0-9_.\\[\\]]+)`, "m"); 105 | 106 | const hintedCallMatch = regExp.exec(src); 107 | 108 | if (hintedCallMatch && hintedCallMatch.length === 2) { 109 | return new VariableSearchResult( 110 | hintedCallMatch[1], 111 | EstimationSource.FunctionDefinition, 112 | valueAssignment 113 | ); 114 | } 115 | return null; 116 | } 117 | 118 | /** 119 | * Detects the type of a value. 120 | * 121 | * @returns The type name, or null if it is not a built-in Python type. 122 | */ 123 | public static detectType(value: string): string | null { 124 | const searches = [ 125 | [ PythonType.List, `${PythonType.List}\\(`, `^ *\\[`], 126 | [ PythonType.Bool, `${PythonType.Bool}\\(`, `^ *(True|False)`], 127 | [ PythonType.Complex, `${PythonType.Complex}\\(`, `^ *[()0-9+*\\/ -.]*[0-9][jJ]`], 128 | [ PythonType.Float, `${PythonType.Float}\\(`, `^ *[-(]*[0-9+*\/ -]*\\.[0-9]`], 129 | [ PythonType.Tuple, `${PythonType.Tuple}\\(`, `^ *\\(([^'",)]+,| *"[^"]*" *,| *'[^']*' *,)`], 130 | [ PythonType.Set, `${PythonType.Set}\\(`, `^ *{( *"[^"]*" *[},]+| *'[^']*' *[},]+|[^:]+[}])`], 131 | [ PythonType.Dict, `${PythonType.Dict}\\(`, "^ *{"], 132 | [ PythonType.String, `${PythonType.String}\\(`, `^ *(['"]{2}|(\\( *)?"[^"]*"|(\\( *)?'[^']*')`], 133 | [ PythonType.Bytes, `${PythonType.Bytes}\\(`, `^ *b['"]`], 134 | [ PythonType.Int, `${PythonType.Int}\\(`, `^ *[-(]*[0-9]`], 135 | [ PythonType.Object, `${PythonType.Object}\\(` ] 136 | ]; 137 | value = value.trim(); 138 | if (value.match("^[a-z]")) { 139 | for (const s of searches) { 140 | const typeName = s[0]; 141 | if (new RegExp(s[1]).test(value)) { 142 | return typeName; 143 | } 144 | } 145 | } 146 | searches.pop(); 147 | for (const s of searches) { 148 | const typeName = s[0]; 149 | if (new RegExp(s[2]).test(value)) { 150 | return typeName; 151 | } 152 | } 153 | return null; 154 | } 155 | 156 | /** 157 | * Searches for a previously hinted parameter with the same name. 158 | * 159 | * @param param The parameter name. 160 | * @param src The source code to search. 161 | * @returns The type hint of the found parameter or null. 162 | */ 163 | public static hintOfSimilarParam(param: string, src: string): string | null { 164 | 165 | const m = new RegExp(`^[ \t]*def [^(\\s]+\\([^)]*\\b${param}\\b: *([^),\\s=#:]+)`, "m").exec(src); 166 | if (m) { 167 | let hint = m[1].trim(); 168 | return hint ? hint : null; 169 | } 170 | return null; 171 | } 172 | 173 | /** 174 | * Searches the result for a terinary operator that might return 2 or more different types. 175 | * 176 | * @param searchResult The search result. 177 | * @returns False if it returns a single type. 178 | */ 179 | public static invalidTernaryOperator(searchResult: VariableSearchResult): boolean { 180 | 181 | if (searchResult.estimationSource === EstimationSource.ClassDefinition) { 182 | return false; 183 | } 184 | const regExp = / if +[^ ]+ +else( +[^ ]+) *$/; 185 | 186 | let ternaryMatch = regExp.exec(searchResult.valueAssignment); 187 | while (ternaryMatch) { 188 | const elseVar = ternaryMatch[1].trim(); 189 | let elseTypeName = this.detectType(elseVar); 190 | 191 | if (elseTypeName) { 192 | ternaryMatch = regExp.exec(elseTypeName); 193 | if (!ternaryMatch) { 194 | return searchResult.typeName !== elseTypeName; 195 | } 196 | } else { 197 | return false; 198 | } 199 | } 200 | return false; 201 | } 202 | 203 | /** 204 | * Detects if a value is imported and returns the imported value. 205 | * For instance, if 'from x import y' is detected for a value of 'x.y', 'y' is returned. 206 | * 207 | * @param value The value. 208 | * @param src The source code to search. 209 | * @param considerAsImports Also search for 'import x as value' imports. 210 | */ 211 | public static findImport(value: string, src: string, considerAsImports: boolean = true): string | null { 212 | 213 | if (value.includes(".")) { 214 | const s = value.split("."); 215 | const type = s[s.length - 1]; 216 | const module = s.slice(0, s.length - 1).join("."); 217 | 218 | let match = null; 219 | 220 | if (s.length === 2 && module !== type) { 221 | // Check if module is a module or a class 222 | match = new RegExp( 223 | `^[ \t]*import +${module}|^[ \t]*from ${moduleName} import (${module})`, "m" 224 | ).exec(src); 225 | if (match) { 226 | return match[1] ? `${match[1]}.${type}` : value; 227 | } 228 | } 229 | match = new RegExp(`^[ \t]*import +${module}|^[ \t]*from ${module} import (${type})`, "m").exec(src); 230 | return match ? match[1] ? type : value : null; 231 | } 232 | return this.isImported(value, src, considerAsImports) ? value : null; 233 | } 234 | 235 | /** 236 | * Detects if a value is imported. 237 | */ 238 | private static isImported(value: string, src: string, checkForAsImports: boolean = true): boolean { 239 | 240 | let exp = `^[ \t]*(from +${moduleName} +import +${value}`; 241 | if (checkForAsImports) { 242 | exp += `|from +${moduleName} +import +${moduleName} +as +${value}`; 243 | } 244 | exp += ")"; 245 | return new RegExp(exp, "m").test(src); 246 | } 247 | 248 | private static isProbablyAClass(lineText: string): boolean { 249 | return new RegExp(`^([a-zA-Z0-9_]+\\.)*[A-Z]`, "m").test(lineText); 250 | } 251 | 252 | /** 253 | * Matches a line that contains 'variableName = (.+)'. 254 | */ 255 | private static variableSearchRegExp(variableName: string): RegExp { 256 | return new RegExp(`^[ \t]*${variableName} *= *(.+)$`, "m"); 257 | } 258 | } -------------------------------------------------------------------------------- /src/typingHintProvider.ts: -------------------------------------------------------------------------------- 1 | import { TypeCategory, PythonType, DataTypeContainer, DataType } from "./python"; 2 | import { capitalized } from "./utils"; 3 | import { VariableSearchResult, TypeSearch } from "./typeSearch"; 4 | 5 | /** 6 | * Provides type hints for the Python typing module. 7 | */ 8 | export class TypingHintProvider { 9 | 10 | private collectionTypes: DataType[]; 11 | private typeContainer: DataTypeContainer; 12 | private typingImportDetected: boolean = false; 13 | private fromTypingImport: boolean = false; 14 | private providedTypes: DataType[] = []; 15 | private typingPrefix: string = "typing"; 16 | 17 | /** 18 | * Constructs a new TypingHintProvider. 19 | * 20 | * @param typeContainer A container with built-in Python types. 21 | */ 22 | constructor(typeContainer: DataTypeContainer) { 23 | this.typeContainer = typeContainer; 24 | this.collectionTypes = Object.values(typeContainer).filter(t => t.category === TypeCategory.Collection); 25 | } 26 | 27 | /** 28 | * Determines if a document contains a typing import. 29 | * 30 | * @param docText The document text to search. 31 | * @returns True if typing is imported. 32 | */ 33 | public detectTypingImport(docText: string): boolean { 34 | if (/^[ \t]*from typing import +([A-Z][a-zA-Z0-9 ,]+)/m.exec(docText)) { 35 | this.fromTypingImport = true; 36 | this.typingImportDetected = true; 37 | } else { 38 | const match = /^[ \t]*(import +typing +as +([a-zA-Z_][a-zA-Z0-9_-]*)|import +typing)/m.exec(docText); 39 | if (match) { 40 | if (match[2]) { 41 | this.typingPrefix = match[2]; 42 | } 43 | this.typingImportDetected = true; 44 | } 45 | } 46 | return this.typingImportDetected; 47 | } 48 | 49 | /** 50 | * Get a hint for a built-in type if typing is imported. 51 | * 52 | * @param type A type name. 53 | * @returns A type hint without a closing bracket, for example 'List[ '. 54 | */ 55 | public getHint(typeName: string): string | null { 56 | const type = this.typeContainer[typeName]; 57 | if (type.category === TypeCategory.Collection) { 58 | this.providedTypes.push(type); 59 | return this.toTypingString(type.name, this.fromTypingImport); 60 | } 61 | return null; 62 | } 63 | 64 | /** 65 | * Get hints if typing is imported. 66 | * 67 | * @param searchResult A search result to derive hints from. 68 | * @returns One or two type hints. For example, 'List[' and 'List[str]'. 69 | */ 70 | public getHints(searchResult: VariableSearchResult | null): string[] | null { 71 | 72 | if (searchResult && searchResult.typeName in this.typeContainer) { 73 | const type = this.typeContainer[searchResult.typeName]; 74 | this.providedTypes.push(type); 75 | 76 | const result: string[] = [ this.toTypingString(type.name, this.fromTypingImport) ]; 77 | let label = result[0]; 78 | 79 | if (type.category === TypeCategory.Collection) { 80 | 81 | // Remove [, {, etc. to detect the type of the first element 82 | let elementValue = searchResult.valueAssignment.trim().substr(1); 83 | let elementType = TypeSearch.detectType(elementValue); 84 | let collectionCount = 1; 85 | let dictElementFound = false; 86 | while ( 87 | !dictElementFound 88 | && elementType 89 | && elementType in this.typeContainer 90 | && this.typeContainer[elementType].category === TypeCategory.Collection 91 | ) { 92 | if (this.typeContainer[elementType].name === PythonType.Dict) { 93 | dictElementFound = true; 94 | } 95 | label += this.toTypingString(elementType, this.fromTypingImport); 96 | elementValue = elementValue.trim().substr(1); 97 | elementType = TypeSearch.detectType(elementValue); 98 | collectionCount++; 99 | } 100 | 101 | let addClosingBrackets = false; 102 | if (elementType && elementType in this.typeContainer) { 103 | label += elementType; 104 | // Detecting the type of dict values here isn't supported, so let the user add them 105 | addClosingBrackets = !dictElementFound && type.name !== PythonType.Dict; 106 | } 107 | if (result[0] !== label) { 108 | if (addClosingBrackets) { 109 | for (let i = 0; i < collectionCount; i++) { 110 | label += "]"; 111 | } 112 | } 113 | result.push(label); 114 | } 115 | return result; 116 | } 117 | } 118 | return null; 119 | } 120 | 121 | /** 122 | * Get hints for collection types that have not been provided yet. 123 | * 124 | * @returns An array of types. 125 | */ 126 | public getRemainingHints(): string[] { 127 | if (!this.typingImportDetected) { 128 | return this.hintsForAllCollectionTypes(); 129 | } 130 | const result: string[] = []; 131 | for (const type of this.collectionTypes) { 132 | if (!this.providedTypes.includes(type)) { 133 | result.push(this.toTypingString(type.name, this.fromTypingImport)); 134 | } 135 | } 136 | return result; 137 | } 138 | 139 | private hintsForAllCollectionTypes(): string[] { 140 | const firstHalf: string[] = []; 141 | const secondHalf: string[] = []; 142 | for (const type of this.collectionTypes) { 143 | if (!this.providedTypes.includes(type)) { 144 | const withoutPrefix = this.fromTypingImport || !this.typingImportDetected; 145 | firstHalf.push(this.toTypingString(type.name, withoutPrefix)); 146 | secondHalf.push(this.toTypingString(type.name, !withoutPrefix)); 147 | } 148 | } 149 | return firstHalf.concat(secondHalf); 150 | } 151 | 152 | private toTypingString(typeName: string, withoutPrefix: boolean): string { 153 | const typingName = capitalized(typeName); 154 | return withoutPrefix ? `${typingName}[` : `${this.typingPrefix}.${typingName}[`; 155 | } 156 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function capitalized(s: string): string { 4 | return s.length > 1 ? s[0].toUpperCase() + s.slice(1) : s; 5 | } -------------------------------------------------------------------------------- /src/workspaceSearcher.ts: -------------------------------------------------------------------------------- 1 | import { CancellationTokenSource, CancellationToken, Uri, workspace } from "vscode"; 2 | import { TypeHintSettings } from "./settings"; 3 | import { TypeSearch } from "./typeSearch"; 4 | import { DataTypeContainer } from "./python"; 5 | 6 | /** 7 | * Searches Python files in the workspace. 8 | */ 9 | export class WorkspaceSearcher { 10 | 11 | private search: boolean = true; 12 | private activeDocUri: Uri; 13 | private typeContainer: DataTypeContainer; 14 | private tokenSource: CancellationTokenSource; 15 | private settings: TypeHintSettings; 16 | 17 | constructor(activeDocumentUri: Uri, settings: TypeHintSettings, typeContainer: DataTypeContainer) { 18 | this.activeDocUri = activeDocumentUri; 19 | this.settings = settings; 20 | this.tokenSource = new CancellationTokenSource(); 21 | this.typeContainer = typeContainer; 22 | } 23 | 24 | /** 25 | * Searches documents, excluding the active one, for a previously hinted parameter with the same name. 26 | * 27 | * @param param The parameter name. 28 | * @param activeDocumentText The source code of the active document. 29 | * @returns The type of the found parameter or null. 30 | */ 31 | public async findHintOfSimilarParam(param: string, activeDocumentText: string): Promise { 32 | this.search = true; 33 | const maxResults = this.settings.workspaceSearchLimit; 34 | 35 | if (maxResults > 0 && workspace.workspaceFolders) { 36 | const uriSplit = this.activeDocUri.path.split("/"); 37 | const glob = `**/${uriSplit[uriSplit.length - 1]}`; 38 | const uris = await workspace.findFiles("**/*.py", glob, maxResults, this.tokenSource.token); 39 | 40 | for (let i = 0; this.search && i < uris.length; i++) { 41 | let doc = await workspace.openTextDocument(uris[i]); 42 | let docText = doc.getText(); 43 | let type = TypeSearch.hintOfSimilarParam(param, docText); 44 | if (this.search && type) { 45 | if (!(type in this.typeContainer)) { 46 | type = TypeSearch.findImport(type, activeDocumentText, false); 47 | } 48 | if (type) { 49 | return type; 50 | } 51 | } 52 | } 53 | } 54 | return null; 55 | } 56 | 57 | /** 58 | * Stops all searches. 59 | */ 60 | public cancel() { 61 | if (this.search) { 62 | this.search = false; 63 | this.tokenSource.cancel(); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /test/common.ts: -------------------------------------------------------------------------------- 1 | import { VariableSearchResult, EstimationSource } from "../src/typeSearch"; 2 | 3 | export interface TestCase { 4 | data: any, 5 | expected: any 6 | } 7 | 8 | export class SetupError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | this.name = "SetupError"; 12 | } 13 | } 14 | 15 | export function messageFor(testData: any, expected: any, actual: any): string { 16 | return `${actual} == ${expected}. \n[Test data]\n${testData}`; 17 | }; 18 | 19 | export function varSearchResult(typeName: string, valueAssignment: string): VariableSearchResult { 20 | return { typeName, estimationSource: EstimationSource.Value, valueAssignment }; 21 | }; -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import { glob } from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | }); 10 | mocha.options.color = true; 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((resolve, reject) => { 15 | 16 | glob('**/**.test.js', { cwd: testsRoot }) 17 | .then((files: string[]) => { 18 | // Add files to the test suite 19 | files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f))); 20 | 21 | try { 22 | // Run the mocha test 23 | mocha.run(failures => { 24 | if (failures > 0) { 25 | reject(new Error(`${failures} tests failed.`)); 26 | } else { 27 | resolve(); 28 | } 29 | }); 30 | } catch (err) { 31 | console.error(err); 32 | reject(err); 33 | } 34 | }) 35 | .catch(reason => reject(reason)); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to the extension test runner script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests( { extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error(err); 19 | console.error('Failed to run tests'); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | // import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /test/suite/providers/completionProvider.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vsc from 'vscode'; 3 | import { paramHintTrigger, PythonType } from "../../../src/python"; 4 | import { CompletionProvider, ParamHintCompletionProvider } from "../../../src/completionProvider"; 5 | import { TypeHintSettings } from '../../../src/settings'; 6 | import { messageFor } from '../../common'; 7 | 8 | suite('ParamHintCompletionProvider', () => { 9 | const provider = new ParamHintCompletionProvider(new TypeHintSettings()); 10 | 11 | test("provides items for first param", async () => { 12 | let param = "param_1:"; 13 | let actual = await providerResult(provider, param); 14 | assert.notStrictEqual(actual, null); 15 | }); 16 | 17 | test("provides items for non-first param", async () => { 18 | let param = "first: str, paramName:"; 19 | let actual = await providerResult(provider, param, "\n\nparamName = 12"); 20 | assert.notStrictEqual(actual, null); 21 | assert.strictEqual(actual?.items[0].label, ` ${PythonType.Int}`); 22 | }); 23 | 24 | test("provides items for param on new line", async () => { 25 | let param = "\n paramName:"; 26 | let actual = await providerResult(provider, param); 27 | assert.notStrictEqual(actual, null); 28 | 29 | param = "\n\tparamName:"; 30 | actual = await providerResult(provider, param); 31 | assert.notStrictEqual(actual, null); 32 | }); 33 | 34 | test("provides items for param with legal non-ascii chars", async () => { 35 | let param = "a変な:"; 36 | let actual = await providerResult(provider, param); 37 | assert.notStrictEqual(actual, null); 38 | }); 39 | 40 | test("provides items for nestled function", async () => { 41 | let data = `): 42 | x = 1 43 | def nestled(multiple_lines, 44 | paramName:`; 45 | let actual = await providerResult(provider, data); 46 | assert.notStrictEqual(actual, null); 47 | }); 48 | 49 | test("provides items for async function", async () => { 50 | let data = "async def func(test:"; 51 | let pos = new vsc.Position(0, data.length); 52 | let expected = null; 53 | let actual = await provideCompletionItems(provider, data, pos); 54 | assert.notStrictEqual(actual, null, messageFor(data, expected, actual)); 55 | 56 | let line2 = " test:"; 57 | data = "async def func(\n" + line2; 58 | pos = new vsc.Position(1, line2.length); 59 | actual = await provideCompletionItems(provider, data, pos); 60 | assert.notStrictEqual(actual, null, messageFor(data, expected, actual)); 61 | }); 62 | 63 | test("provides default items if nothing is detected", async () => { 64 | let param = "notFound:"; 65 | let expected = typeHints().concat(typingHints()); 66 | let result = await providerResult(provider, param); 67 | 68 | assert.notStrictEqual(result, null); 69 | const actual: string[] | undefined = result?.items.map(item => item.label.toString().trim()); 70 | assert.deepStrictEqual(actual, expected); 71 | }); 72 | 73 | test("provides type estimations + default items", async () => { 74 | let param = "param:"; 75 | let expected = ["Class"].concat(typeHints()).concat(typingHints()); 76 | 77 | let result = await providerResult(provider, param, "\n\nparam = Class()"); 78 | 79 | assert.notStrictEqual(result, null); 80 | const actual: string[] | undefined = result?.items.map(item => item.label.toString().trim()); 81 | assert.deepStrictEqual(actual, expected); 82 | }); 83 | 84 | test("does not provide items unless a function def is detected", async () => { 85 | let text = " :"; 86 | let pos = new vsc.Position(0, text.length); 87 | let actual = await provideCompletionItems(provider, text, pos); 88 | assert.strictEqual(actual, null); 89 | }); 90 | 91 | test("does not provide items for ':' without a param (within function brackets)", async () => { 92 | let actual = await providerResult(provider, "param, :"); 93 | assert.strictEqual(actual, null); 94 | }); 95 | 96 | test("does not provide items for ':' under a function def", async () => { 97 | let data = "):\n d = ', not_a_param:"; 98 | let expected = null; 99 | let actual = await providerResult(provider, data); 100 | assert.strictEqual(actual, expected, messageFor(data, expected, actual)); 101 | 102 | data = "):\n :"; 103 | actual = await providerResult(provider, data); 104 | assert.strictEqual(actual, expected, messageFor(data, expected, actual)); 105 | 106 | data = "):\n d = { key:"; 107 | actual = await providerResult(provider, data); 108 | assert.strictEqual(actual, null, messageFor(data, expected, actual)); 109 | 110 | data = `self, 111 | s: str, 112 | f: float, 113 | i: int): 114 | v = ', not_a_param:`; 115 | actual = await providerResult(provider, data); 116 | assert.strictEqual(actual, null, messageFor(data, expected, actual)); 117 | 118 | data = `self, 119 | s: str, 120 | f: float, 121 | i: int) -> int: 122 | v = ', not_a_param:`; 123 | actual = await providerResult(provider, data); 124 | assert.strictEqual(actual, null, messageFor(data, expected, actual)); 125 | 126 | data = `self, 127 | s: str, 128 | f: float, 129 | i: int) -> 変な: 130 | v = ', not_a_param:`; 131 | actual = await providerResult(provider, data); 132 | assert.strictEqual(actual, null, messageFor(data, expected, actual)); 133 | }); 134 | 135 | test("does not provide items for end of function definition", async () => { 136 | let actual = await providerResult(provider, "):"); 137 | assert.strictEqual(actual, null); 138 | }); 139 | 140 | test("does not provide items for end of function definition with typing hint", async () => { 141 | let actual = await providerResult(provider, "def test(n: int) -> List[str]:"); 142 | assert.strictEqual(actual, null); 143 | }); 144 | 145 | test("does not provide items for end of function definition with typing Dict hint", async () => { 146 | let actual = await providerResult(provider, "def test(n: int) -> Dict[str, str]:"); 147 | assert.strictEqual(actual, null); 148 | }); 149 | 150 | test("does not include * in parameter name", async () => { 151 | let param = "*paramName:"; 152 | let actual = await providerResult(provider, param, "\n\nparamName = 12"); 153 | assert.strictEqual(actual?.items[0].label, ` ${PythonType.Int}`); 154 | }); 155 | 156 | }); 157 | 158 | const language = "python"; 159 | 160 | async function providerResult( 161 | provider: CompletionProvider, 162 | functionText: string, 163 | trailingText?: string 164 | ): Promise { 165 | let content = ` def func(${functionText}`; 166 | const lines: string[] = content.split("\n"); 167 | const lastLineIdx = lines.length - 1; 168 | const lastPos = new vsc.Position(lastLineIdx, lines[lastLineIdx].length); 169 | 170 | if (trailingText) { 171 | content += trailingText; 172 | } 173 | 174 | const doc = await vsc.workspace.openTextDocument({ language, content }); 175 | const token = new vsc.CancellationTokenSource().token; 176 | const ctx = { triggerCharacter: paramHintTrigger, triggerKind: vsc.CompletionTriggerKind.TriggerCharacter }; 177 | 178 | return provider.provideCompletionItems(doc, lastPos, token, ctx); 179 | } 180 | 181 | async function provideCompletionItems( 182 | provider: CompletionProvider, 183 | documentContent: string, 184 | pos: vsc.Position 185 | ): Promise { 186 | const doc = await vsc.workspace.openTextDocument({ language, content: documentContent }); 187 | const token = new vsc.CancellationTokenSource().token; 188 | const ctx = { triggerCharacter: paramHintTrigger, triggerKind: vsc.CompletionTriggerKind.TriggerCharacter }; 189 | 190 | return provider.provideCompletionItems(doc, pos, token, ctx); 191 | } 192 | 193 | const typeHints = (): string[] => Object.values(PythonType).sort(); 194 | 195 | const typingHints = (): string[] => { 196 | const prefix = "typing."; 197 | return [ 198 | `Dict[`, 199 | `List[`, 200 | `Set[`, 201 | `Tuple[`, 202 | `${prefix}Dict[`, 203 | `${prefix}List[`, 204 | `${prefix}Set[`, 205 | `${prefix}Tuple[` 206 | ]; 207 | }; -------------------------------------------------------------------------------- /test/suite/providers/typeHintProvider.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { getDataTypeContainer } from "../../../src/python"; 3 | import { TypeHintProvider } from "../../../src/typeHintProvider"; 4 | import { messageFor } from "../../common"; 5 | 6 | suite('TypeHintProvider', () => { 7 | 8 | let provider: TypeHintProvider; 9 | 10 | setup(() => { 11 | provider = new TypeHintProvider(getDataTypeContainer()); 12 | }); 13 | 14 | suite('estimateTypeHints', () => { 15 | 16 | test("returns type of a variable with the same name", async () => { 17 | let param = "param"; 18 | let documentText = `def test(param:\n\n${param} = 12`; 19 | let expected = ["int"]; 20 | let actual = await provider.estimateTypeHints(param, documentText); 21 | assert.deepStrictEqual(actual, expected); 22 | }); 23 | 24 | test("returns [type, typing equivalents] of a collection variable with the same name", async () => { 25 | let param = "param"; 26 | let documentText = `from typing import List\ndef test(param:\n\n${param} = [12]`; 27 | let expected = 3; 28 | let result = await provider.estimateTypeHints(param, documentText); 29 | assert.strictEqual(result.length, expected); 30 | }); 31 | 32 | test("does not return duplicates as a result of a type guess", async () => { 33 | let param = "text"; 34 | let documentText = `def test(param:\n\n${param} = 'this.typeGuessFor() returns str for the param name'`; 35 | let expected = ["str"]; 36 | let actual = await provider.estimateTypeHints(param, documentText); 37 | assert.deepStrictEqual(actual, expected); 38 | }); 39 | 40 | test("returns type of a variable and a type guess, if the types differ", async () => { 41 | let param = "number"; 42 | let documentText = `def test(param:\n\n${param} = 1.23`; 43 | let expected = 2; 44 | let result = await provider.estimateTypeHints(param, documentText); 45 | assert.strictEqual(result.length, expected); 46 | }); 47 | 48 | test("returns class with same name", async () => { 49 | let param = "test"; 50 | let documentText = `class Test:`; 51 | let expected = ["Test"]; 52 | let actual = await provider.estimateTypeHints(param, documentText); 53 | assert.deepStrictEqual(actual, expected); 54 | }); 55 | 56 | test("returns hint of similar param", async () => { 57 | let param = "test"; 58 | let hint = "str"; 59 | let documentText = `def func(test: ${hint}):`; 60 | let expected = [hint]; 61 | let actual = await provider.estimateTypeHints(param, documentText); 62 | assert.deepStrictEqual(actual, expected); 63 | }); 64 | 65 | test("returns type if param name ends with it", async () => { 66 | let param = "test_str"; 67 | let expected = ["str"]; 68 | let actual = await provider.estimateTypeHints(param, ""); 69 | assert.deepStrictEqual(actual, expected, messageFor(param, expected, actual)); 70 | 71 | provider = new TypeHintProvider(getDataTypeContainer()); 72 | param = "teststr"; 73 | actual = await provider.estimateTypeHints(param, ""); 74 | assert.deepStrictEqual(actual, expected, messageFor(param, expected, actual)); 75 | }); 76 | 77 | test("returns [type, typing equivalent] if param name ends with collection type", async () => { 78 | let param = "test_list"; 79 | let documentText = `from typing import List`; 80 | let expected = ["list", "List["]; 81 | let actual = await provider.estimateTypeHints(param, documentText); 82 | assert.deepStrictEqual(actual, expected, messageFor(param, expected, actual)); 83 | 84 | provider = new TypeHintProvider(getDataTypeContainer()); 85 | param = "testlist"; 86 | actual = await provider.estimateTypeHints(param, documentText); 87 | assert.deepStrictEqual(actual, expected, messageFor(param, expected, actual)); 88 | }); 89 | 90 | }); 91 | 92 | }); -------------------------------------------------------------------------------- /test/suite/providers/typingHintProvider.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { getDataTypeContainer, DataType, PythonType } from "../../../src/python"; 3 | import { TypingHintProvider } from "../../../src/typingHintProvider"; 4 | import { TypeHintSettings } from '../../../src/settings'; 5 | import { SetupError, varSearchResult, messageFor, TestCase } from '../../common'; 6 | 7 | suite('TypingHintProvider', () => { 8 | 9 | const importTyping: string = "import x\nimport typing"; 10 | const importTypingAsX: string = "import x\nimport typing as x"; 11 | const fromTypingImport: string = "import x\nfrom typing import List, Dict, Tuple, Set"; 12 | const typeContainer = getDataTypeContainer(); 13 | 14 | suite('containsTyping', () => { 15 | 16 | let provider: TypingHintProvider; 17 | 18 | setup(() => { 19 | provider = new TypingHintProvider(typeContainer); 20 | }); 21 | 22 | test("returns true for 'import typing'", async () => { 23 | const actual = await provider.detectTypingImport(importTyping); 24 | assert.strictEqual(actual, true); 25 | }); 26 | 27 | test("returns true for 'import typing as x'", async () => { 28 | const actual = await provider.detectTypingImport(importTypingAsX); 29 | assert.strictEqual(actual, true); 30 | }); 31 | 32 | test("returns true for 'from typing import x'", async () => { 33 | const actual = await provider.detectTypingImport(fromTypingImport); 34 | assert.strictEqual(actual, true); 35 | }); 36 | }); 37 | 38 | suite("getHint", () => { 39 | 40 | let provider: TypingHintProvider; 41 | 42 | setup(() => { 43 | provider = new TypingHintProvider(typeContainer); 44 | }); 45 | 46 | test("returns typing.Type[", async () => { 47 | const expected = "typing.List["; 48 | runTest(await provider.detectTypingImport(importTyping), getHintTest, provider, expected); 49 | }); 50 | 51 | test("returns x.Type[ for 'import typing as x'", async () => { 52 | const expected = "x.List["; 53 | runTest(await provider.detectTypingImport(importTypingAsX), getHintTest, provider, expected); 54 | }); 55 | 56 | test("returns Type[ for 'from typing' import", async () => { 57 | const expected = "List["; 58 | runTest(await provider.detectTypingImport(fromTypingImport), getHintTest, provider, expected); 59 | }); 60 | 61 | function getHintTest(provider: TypingHintProvider, expected: string) { 62 | const actual = provider.getHint(PythonType.List); 63 | assert.strictEqual(actual, expected); 64 | } 65 | }); 66 | 67 | suite("getHints", () => { 68 | 69 | const provider = new TypingHintProvider(typeContainer); 70 | let typingImported: boolean; 71 | 72 | setup(async () => { 73 | typingImported = await provider.detectTypingImport(fromTypingImport); 74 | }); 75 | 76 | test("returns Type[ for empty collection", () => { 77 | const data = "[]"; 78 | const expected = ["List["]; 79 | runTest(typingImported, getHintsTest, provider, {data, expected }, PythonType.List); 80 | }); 81 | 82 | test("returns Dict[ and 'Dict[key,' for dicts", () => { 83 | let data = "{ 1: 2 }"; 84 | let expected = ["Dict[", "Dict[int"]; 85 | runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.Dict); 86 | }); 87 | 88 | test("handles nestled dicts", () => { 89 | let data = "[ { 1: 2 } ]"; 90 | let expected = ["List[", "List[Dict[int"]; 91 | runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.List); 92 | }); 93 | 94 | test("returns Type[ and Type[type] for non-dicts", () => { 95 | let data = "['str']"; 96 | let expected = ["List[", "List[str]"]; 97 | runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.List); 98 | 99 | data = "(1, {'ignore': 'this'})"; 100 | expected = ["Tuple[", "Tuple[int]"]; 101 | runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.Tuple); 102 | 103 | data = "{ 1, 2 }"; 104 | expected = ["Set[", "Set[int]"]; 105 | runTest(typingImported, getHintsTest, provider, { data, expected }, PythonType.Set); 106 | }); 107 | 108 | test("adds typing prefixes for 'import typing' imports", async () => { 109 | let p = new TypingHintProvider(typeContainer); 110 | let data = "[ { 1: 2 } ]"; 111 | let expected = ["typing.List[", "typing.List[typing.Dict[int"]; 112 | runTest( 113 | await p.detectTypingImport(importTyping), 114 | getHintsTest, 115 | p, 116 | { data, expected }, 117 | PythonType.List 118 | ); 119 | }); 120 | 121 | function getHintsTest(provider: TypingHintProvider, testCase: TestCase, type: PythonType) { 122 | const actual = provider.getHints(varSearchResult(type, testCase.data)); 123 | assert.deepStrictEqual(actual, testCase.expected, messageFor(testCase.data, testCase.expected, actual)); 124 | } 125 | }); 126 | 127 | suite("getRemainingHints", () => { 128 | 129 | let provider: TypingHintProvider; 130 | 131 | setup(() => { 132 | provider = new TypingHintProvider(typeContainer); 133 | }); 134 | 135 | test("returns all typing hints if typing is not imported, without prefix followed by with prefix", async () => { 136 | await provider.detectTypingImport(""); 137 | const expected = typingTypes(false).concat(typingTypes(true)); 138 | const actual = provider.getRemainingHints(); 139 | assert.deepStrictEqual(actual, expected); 140 | }); 141 | 142 | test("returns hints without prefix first, for from typing import", async () => { 143 | const expected = typingTypes(false); 144 | runTest( 145 | await provider.detectTypingImport(fromTypingImport), 146 | getRemainingHintsTest, 147 | provider, 148 | expected 149 | ); 150 | }); 151 | 152 | test("returns hints with prefix first, if imported", async () => { 153 | const expected = typingTypes(true); 154 | runTest( 155 | await provider.detectTypingImport(importTyping), 156 | getRemainingHintsTest, 157 | provider, 158 | expected 159 | ); 160 | }); 161 | 162 | 163 | function getRemainingHintsTest(provider: TypingHintProvider, expected: string) { 164 | const actual = provider.getRemainingHints(); 165 | assert.deepStrictEqual(actual, expected); 166 | } 167 | }); 168 | 169 | const typingTypes = (withPrefix: boolean) => { 170 | const prefix = withPrefix ? "typing." : ""; 171 | return [`${prefix}Dict[`, `${prefix}List[`, `${prefix}Set[`, `${prefix}Tuple[` ]; 172 | }; 173 | 174 | function runTest(typingDetected: boolean, test: (...params: any) => void, ...args: any) { 175 | if (typingDetected) { 176 | test(...args); 177 | } else { 178 | throw new SetupError("The provider failed to detect a typing import."); 179 | } 180 | } 181 | }); -------------------------------------------------------------------------------- /test/suite/typeSearch/classWithSameName.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { TypeSearch } from "../../../src/typeSearch"; 3 | import { messageFor } from "../../common"; 4 | 5 | suite('TypeSearch.classWithSameName', () => { 6 | 7 | test("finds class", () => { 8 | let value = "test"; 9 | let expected = "Test"; 10 | let src = `class ${expected}:`; 11 | let actual = TypeSearch.classWithSameName(value, src); 12 | assert.strictEqual(actual, expected); 13 | }); 14 | 15 | test("finds subclass", () => { 16 | let value = "test"; 17 | let expected = "Test"; 18 | let src = `class ${expected}(Super):`; 19 | let actual = TypeSearch.classWithSameName(value, src); 20 | assert.strictEqual(actual, expected); 21 | }); 22 | 23 | test("handles tabs and spaces", () => { 24 | let value = "test"; 25 | let expected = "Test"; 26 | let src = `\tclass ${expected}:`; 27 | let actual = TypeSearch.classWithSameName(value, src); 28 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 29 | 30 | src = ` class ${expected}:`; 31 | actual = TypeSearch.classWithSameName(value, src); 32 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/suite/typeSearch/detection.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { messageFor, TestCase } from "../../common"; 3 | import { TypeSearch } from "../../../src/typeSearch"; 4 | 5 | suite('TypeSearch.detectType', () => { 6 | 7 | test("detects ints", async () => { 8 | const expected = "int"; 9 | 10 | let src = "11"; 11 | let actual = TypeSearch.detectType(src); 12 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 13 | 14 | src = "-11"; 15 | actual = TypeSearch.detectType(src); 16 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 17 | 18 | src = "0b10"; 19 | actual = TypeSearch.detectType(src); 20 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 21 | 22 | src = "0o10"; 23 | actual = TypeSearch.detectType(src); 24 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 25 | 26 | src = "0x10"; 27 | actual = TypeSearch.detectType(src); 28 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 29 | }); 30 | 31 | 32 | test("detects floats", async () => { 33 | const expected = "float"; 34 | 35 | let src = "12.3"; 36 | let actual = TypeSearch.detectType(src); 37 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 38 | 39 | src = ".3"; 40 | actual = TypeSearch.detectType(src); 41 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 42 | 43 | src = "-.3"; 44 | actual = TypeSearch.detectType(src); 45 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 46 | 47 | src = "1 + 2 - 1 * 2 / 2.0"; 48 | actual = TypeSearch.detectType(src); 49 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 50 | }); 51 | 52 | test("detects complex numbers", async () => { 53 | const expected = "complex"; 54 | 55 | let src = "0+1.1-2*3/4j"; 56 | let actual = TypeSearch.detectType(src); 57 | assert.strictEqual(actual, expected); 58 | 59 | }); 60 | 61 | test("detects strings", async () => { 62 | const expected = "str"; 63 | 64 | let src = "'test'"; 65 | let actual = TypeSearch.detectType(src); 66 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 67 | 68 | src = "\"test\"'"; 69 | actual = TypeSearch.detectType(src); 70 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 71 | 72 | src = "('test')"; 73 | actual = TypeSearch.detectType(src); 74 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 75 | 76 | src = "'''t\nest''')"; 77 | actual = TypeSearch.detectType(src); 78 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 79 | }); 80 | 81 | test("detects bools", async () => { 82 | const expected = "bool"; 83 | 84 | let src = "True"; 85 | let actual = TypeSearch.detectType(src); 86 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 87 | 88 | src = "False"; 89 | actual = TypeSearch.detectType(src); 90 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 91 | }); 92 | 93 | test("detects lists", async () => { 94 | const expected = "list"; 95 | 96 | let src = "["; 97 | let actual = TypeSearch.detectType(src); 98 | assert.strictEqual(actual, expected); 99 | }); 100 | 101 | test("detects dicts", async () => { 102 | const expected = "dict"; 103 | 104 | let src = "{ 5: 'j'}"; 105 | let actual = TypeSearch.detectType(src); 106 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 107 | 108 | src = "{ (1,2): (3) }"; 109 | actual = TypeSearch.detectType(src); 110 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 111 | 112 | src = "{'':11}"; 113 | actual = TypeSearch.detectType(src); 114 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 115 | }); 116 | 117 | test("detects tuples", async () => { 118 | const expected = "tuple"; 119 | 120 | let src = "('dont return str please', 'ok')"; 121 | let actual = TypeSearch.detectType(src); 122 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 123 | 124 | src = " ( '4' , '5' )"; 125 | actual = TypeSearch.detectType(src); 126 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 127 | }); 128 | 129 | test("detects sets", async () => { 130 | const expected = "set"; 131 | 132 | let src = "{'dont return dict or string please'}"; 133 | let actual = TypeSearch.detectType(src); 134 | assert.strictEqual(actual, expected); 135 | 136 | src = "{1, 2}"; 137 | actual = TypeSearch.detectType(src); 138 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 139 | 140 | src = "{1 , 2}"; 141 | actual = TypeSearch.detectType(src); 142 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 143 | }); 144 | 145 | test("detects bytes", async () => { 146 | const expected = "bytes"; 147 | 148 | let src = "b'dont return string please'"; 149 | let actual = TypeSearch.detectType(src); 150 | assert.strictEqual(actual, expected); 151 | 152 | src = 'b"dont return string please"'; 153 | actual = TypeSearch.detectType(src); 154 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 155 | 156 | src = "b'''hi'''"; 157 | actual = TypeSearch.detectType(src); 158 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 159 | 160 | src = 'b"""hi"""'; 161 | actual = TypeSearch.detectType(src); 162 | assert.strictEqual(TypeSearch.detectType(src), expected, messageFor(src, expected, actual)); 163 | }); 164 | 165 | test("detects type() call", () => { 166 | const testCases: TestCase[] = [ 167 | { data: "int('2')", expected: "int" }, 168 | { data: "bool('true')", expected: "bool" }, 169 | { data: "list(foo)", expected: "list" }, 170 | { data: "dict(foo)", expected: "dict" }, 171 | { data: "tuple(foo)", expected: "tuple" }, 172 | { data: "str(1)", expected: "str" }, 173 | { data: "set([1])", expected: "set" }, 174 | { data: "bytes('hi', encoding='utf-8')", expected: "bytes" } 175 | ]; 176 | for (const c of testCases) { 177 | let actual = TypeSearch.detectType(c.data); 178 | assert.strictEqual(actual, c.expected); 179 | } 180 | }); 181 | 182 | test("ignores spaces", async () => { 183 | const expected = "int"; 184 | 185 | let src = " 5"; 186 | let actual = TypeSearch.detectType(src); 187 | assert.strictEqual(actual, expected); 188 | }); 189 | }); 190 | 191 | suite('TypeSearch.variableWithSameName', function() { 192 | 193 | test("detects class if defined in the document", async () => { 194 | const expected = "test_class"; 195 | const line = "var = test_class()"; 196 | let src = `class ${expected}:\n\tpass\n${line}`; 197 | let param = "var"; 198 | let actual = await TypeSearch.variableWithSameName(param, src); 199 | 200 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 201 | }); 202 | 203 | test("detects variable types if initialized in the document", async () => { 204 | const expected = "int"; 205 | const line = "var = x"; 206 | 207 | // Variable with the same name initialized above 208 | let src = `${line}\n\ndef main():\n\tx = 5`; 209 | let param = "var"; 210 | let actual = await TypeSearch.variableWithSameName(param, src); 211 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 212 | 213 | // Variable with the same name initialized below 214 | src = `x = 5\n${line}`; 215 | actual = await TypeSearch.variableWithSameName(param, src); 216 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 217 | }); 218 | 219 | test("considers value to be an object initializtion if title case", async () => { 220 | let expected = "TestClass"; 221 | let src = `var = ${expected}(x)`; 222 | let param = "var"; 223 | let actual = await TypeSearch.variableWithSameName(param, src); 224 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 225 | 226 | expected = "test.test.TestClass"; 227 | src = `var = ${expected}(x)`; 228 | actual = await TypeSearch.variableWithSameName(param, src); 229 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 230 | }); 231 | 232 | test("returns null for title case functions (if the function is defined in the document)", async () => { 233 | const expected = null; 234 | const line = "var = Func(x)"; 235 | 236 | let src = `def Func(x):\n\tpass\n\n${line}`; 237 | let param = "var"; 238 | let actual = await TypeSearch.variableWithSameName(param, src); 239 | assert.strictEqual(actual, expected); 240 | }); 241 | 242 | test("detects function return values if type hinted", async () => { 243 | const expected = "int"; 244 | let src = "def test() -> int:\n\treturn 1\n\nvar = test()"; 245 | let param = "var"; 246 | let actual = await TypeSearch.variableWithSameName(param, src); 247 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 248 | 249 | src = "def test(self) -> int:\n\treturn 1\n\nvar = cls.test()"; 250 | actual = await TypeSearch.variableWithSameName(param, src); 251 | assert.strictEqual(actual?.typeName, expected, messageFor(src, expected, actual)); 252 | }); 253 | 254 | test("doesn't consider function calls to be variables", async () => { 255 | const expected = null; 256 | let src = `obj = call()`; 257 | let param = "var"; 258 | let actual = await TypeSearch.variableWithSameName(param, src); 259 | assert.strictEqual(actual, expected); 260 | }); 261 | 262 | test("ignores single-line comments", async () => { 263 | const expected = null; 264 | let line = "var = obj"; 265 | let src = `# obj = 5\n${line}`; 266 | let param = "var"; 267 | let actual = await TypeSearch.variableWithSameName(param, src); 268 | assert.strictEqual(actual, expected); 269 | }); 270 | }); -------------------------------------------------------------------------------- /test/suite/typeSearch/findImport.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { TypeSearch } from "../../../src/typeSearch"; 3 | import { messageFor } from '../../common'; 4 | 5 | suite('TypeSearch.findImport', () => { 6 | 7 | test("finds import x", () => { 8 | let value = "module_x.Type"; 9 | let expected = value; 10 | let src = "import module_x"; 11 | let actual = TypeSearch.findImport(value, src, false); 12 | assert.strictEqual(actual, expected); 13 | }); 14 | 15 | test("finds import x.y", () => { 16 | let value = "package.x.y.Type"; 17 | let expected = value; 18 | let src = "import package.x.y"; 19 | let actual = TypeSearch.findImport(value, src, false); 20 | assert.strictEqual(actual, expected); 21 | }); 22 | 23 | test("finds import x.y.(...)z", () => { 24 | let value = "package.x.y.z.a.Type"; 25 | let expected = value; 26 | let src = "import package.x.y.z.a"; 27 | let actual = TypeSearch.findImport(value, src, false); 28 | assert.strictEqual(actual, expected); 29 | }); 30 | 31 | test("finds from x import", () => { 32 | let value = "var"; 33 | let expected = value; 34 | let src = "import something\nfrom something import " + expected; 35 | let actual = TypeSearch.findImport(value, src, false); 36 | assert.strictEqual(actual, expected); 37 | }); 38 | 39 | test("finds from x import y as z", () => { 40 | let value = "var"; 41 | let expected = value; 42 | let src = "from pkg import y as " + expected; 43 | let actual = TypeSearch.findImport(value, src, true); 44 | assert.strictEqual(actual, expected); 45 | }); 46 | 47 | 48 | test("value == module.Type --> returns Type for 'from module import Type'", () => { 49 | let value = "module.Type"; 50 | let expected = "Type"; 51 | let src = "from module import " + expected; 52 | let actual = TypeSearch.findImport(value, src, false); 53 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 54 | 55 | value = "package.x.y_test.Type"; 56 | src = "from package.x.y_test import " + expected; 57 | actual = TypeSearch.findImport(value, src, false); 58 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 59 | }); 60 | 61 | test("value == x.Type --> returns x.Type for 'from y import x'", () => { 62 | let value = "x.Type"; 63 | let expected = value; 64 | let src = "from y import x"; 65 | let actual = TypeSearch.findImport(value, src, false); 66 | assert.strictEqual(actual, expected); 67 | }); 68 | 69 | test("ignores extra spaces and tabs", () => { 70 | let value = "var"; 71 | let expected = value; 72 | let src = "def x():\n\t from pkg import " + expected; 73 | let actual = TypeSearch.findImport(value, src, false); 74 | assert.strictEqual(actual, expected); 75 | }); 76 | 77 | }); -------------------------------------------------------------------------------- /test/suite/typeSearch/hintOfSimilarParam.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { messageFor, TestCase } from "../../common"; 3 | import { TypeSearch } from "../../../src/typeSearch"; 4 | 5 | suite('TypeSearch.hintOfSimilarParam', () => { 6 | 7 | test("finds hint of lone param", () => { 8 | const expected = "str"; 9 | const param = "test"; 10 | let src = `def func(${param}: str):\ndef test(${param}:`; 11 | let actual = TypeSearch.hintOfSimilarParam(param, src); 12 | assert.strictEqual(actual, expected); 13 | }); 14 | 15 | test("finds hint of param with preceding parameters", () => { 16 | const expected = "str"; 17 | const param = "test"; 18 | 19 | let src = `def func(self, p1: int,${param}: str):\ndef test(${param}:`; 20 | let actual = TypeSearch.hintOfSimilarParam(param, src); 21 | assert.strictEqual(actual, expected); 22 | }); 23 | 24 | test("finds hint of param with trailing parameters", () => { 25 | const expected = "str"; 26 | const param = "test"; 27 | 28 | let src = `def func(self, ${param}: str,new: int):\ndef test(${param}:`; 29 | let actual = TypeSearch.hintOfSimilarParam(param, src); 30 | assert.strictEqual(actual, expected); 31 | }); 32 | 33 | test("handles line breaks in function definition", () => { 34 | const expected = "str"; 35 | const param = "test"; 36 | 37 | let src = `def func(\n\t${param}: str,new: int):\ndef test(${param}:`; 38 | let actual = TypeSearch.hintOfSimilarParam(param, src); 39 | assert.strictEqual(actual, expected); 40 | }); 41 | 42 | test("excludes default values", () => { 43 | const expected = "str"; 44 | const param = "test"; 45 | 46 | let src = `def func(${param}: str='exclude',new: int):\ndef test(${param}:`; 47 | let actual = TypeSearch.hintOfSimilarParam(param, src); 48 | assert.strictEqual(actual, expected); 49 | }); 50 | 51 | test("finds non-ascii hint", () => { 52 | const expected = "蟒蛇"; 53 | const param = "test"; 54 | let src = `def func(${param}: 蟒蛇):\ndef test(${param}:`; 55 | let actual = TypeSearch.hintOfSimilarParam(param, src); 56 | assert.strictEqual(actual, expected); 57 | }); 58 | 59 | test("matches non-ascii function names", () => { 60 | const expected = "str"; 61 | const param = "test"; 62 | let src = `def 蟒蛇(${param}: str):\ndef test(${param}:`; 63 | let actual = TypeSearch.hintOfSimilarParam(param, src); 64 | assert.strictEqual(actual, expected); 65 | }); 66 | 67 | test("doesn't match param name within other text", () => { 68 | const expected = null; 69 | const param = "test"; 70 | 71 | let src = `def func(text${param}: str,new: int):\ndef test(${param}:`; 72 | let actual = TypeSearch.hintOfSimilarParam(param, src); 73 | assert.strictEqual(actual, expected); 74 | }); 75 | 76 | test("doesn't provide items for ':' followed by ':'", () => { 77 | 78 | const expected = null; 79 | const param = "test"; 80 | 81 | let src = `def func(${param}::):\ndef test(${param}:`; 82 | let actual = TypeSearch.hintOfSimilarParam(param, src); 83 | assert.strictEqual(actual, expected); 84 | }); 85 | 86 | test("doesn't match comments", () => { 87 | const expected = null; 88 | const param = "test"; 89 | let src = `# def func(${param}: str):\ndef test(${param}:`; 90 | let actual = TypeSearch.hintOfSimilarParam(param, src); 91 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 92 | 93 | src = `def func(\n\t# {${param}: 123}`; 94 | assert.strictEqual(actual, expected, messageFor(src, expected, actual)); 95 | }); 96 | 97 | }); -------------------------------------------------------------------------------- /test/suite/typeSearch/invalidTernaryOperator.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { TypeSearch, EstimationSource } from "../../../src/typeSearch"; 3 | import { varSearchResult } from '../../common'; 4 | 5 | suite('TypeSearch.invalidTernaryOperator', () => { 6 | 7 | test("returns true for invalid operator", () => { 8 | 9 | let expected = true; 10 | let typeName = "int"; 11 | let src = "var = 1 if ok else 2.213"; 12 | let actual = TypeSearch.invalidTernaryOperator(varSearchResult(typeName, src)); 13 | assert.strictEqual(actual, expected); 14 | }); 15 | 16 | test("returns true for invalid nestled operator", () => { 17 | let expected = true; 18 | let typeName = "int"; 19 | let src = "var = 1 if ok else 2 if True else 'false'"; 20 | let actual = TypeSearch.invalidTernaryOperator(varSearchResult(typeName, src)); 21 | assert.strictEqual(actual, expected); 22 | }); 23 | 24 | test("returns false for valid single operator", () => { 25 | 26 | let expected = false; 27 | let typeName = "int"; 28 | let src = "var = 1 if ok else 2"; 29 | let actual = TypeSearch.invalidTernaryOperator(varSearchResult(typeName, src)); 30 | assert.strictEqual(actual, expected); 31 | }); 32 | 33 | test("returns false for valid nestled operator", () => { 34 | let expected = false; 35 | let typeName = "int"; 36 | let src = "var = 1 if ok else 2 if True else 9"; 37 | let actual = TypeSearch.invalidTernaryOperator(varSearchResult(typeName, src)); 38 | assert.strictEqual(actual, expected); 39 | }); 40 | 41 | }); -------------------------------------------------------------------------------- /test/suite/workspaceSearcher.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vsc from 'vscode'; 3 | import { getDataTypeContainer } from '../../src/python'; 4 | import { TypeHintSettings } from '../../src/settings'; 5 | import { WorkspaceSearcher } from "../../src/workspaceSearcher"; 6 | import { messageFor } from "../common"; 7 | 8 | suite('findHintOfSimilarParam', () => { 9 | 10 | test("doesn't search if workspaceFolders is undefined", async () => { 11 | const param = "i"; 12 | const activeDocument = `def func(${param}: int):\ndef test(${param}:`; 13 | const searcher = await newWorkspaceSearcher(activeDocument); 14 | 15 | const actual = await searcher.findHintOfSimilarParam(param, activeDocument); 16 | 17 | assert.strictEqual(actual, null); 18 | }); 19 | }); 20 | 21 | const language = "python"; 22 | 23 | async function addDocumentToWorkspace(documentText: string) { 24 | await vsc.workspace.openTextDocument({ language, content: documentText }); 25 | } 26 | 27 | async function setup() { 28 | var r = vsc.workspace.updateWorkspaceFolders(0, undefined, { 29 | uri: vsc.Uri.parse(`${__dirname}`), 30 | name: "test" 31 | }); 32 | for (let i = 0; i < 4; i++) { 33 | await vsc.workspace.openTextDocument({ language, content: "pass" }); 34 | } 35 | } 36 | 37 | async function newWorkspaceSearcher(documentContent: string): Promise { 38 | const doc = await vsc.workspace.openTextDocument({ language, content: documentContent }); 39 | return new WorkspaceSearcher(doc.uri, new TypeHintSettings(), getDataTypeContainer()); 40 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": ".", 11 | "strict": true, /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */ 14 | 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | ".vscode-test" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'extension.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]' 18 | }, 19 | devtool: 'source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; --------------------------------------------------------------------------------