├── .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 | 
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 |
40 |
41 |
42 |
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;
--------------------------------------------------------------------------------