chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
## DO NOT MODIFY THIS FILE MANUALLY. This is part of auto-baselining from 1ES Pipeline Templates. Go to [https://aka.ms/1espt-autobaselining] for more details.
|
||||
|
||||
pipelines:
|
||||
111:
|
||||
retail:
|
||||
source:
|
||||
credscan:
|
||||
lastModifiedDate: 2024-09-10
|
||||
eslint:
|
||||
lastModifiedDate: 2024-09-10
|
||||
psscriptanalyzer:
|
||||
lastModifiedDate: 2024-09-10
|
||||
armory:
|
||||
lastModifiedDate: 2024-09-10
|
||||
binary:
|
||||
credscan:
|
||||
lastModifiedDate: 2025-02-04
|
||||
binskim:
|
||||
lastModifiedDate: 2025-02-04
|
||||
spotbugs:
|
||||
lastModifiedDate: 2025-02-04
|
||||
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"properties": {
|
||||
"helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"baselines": {
|
||||
"default": {
|
||||
"name": "default",
|
||||
"createdDate": "2025-01-28 06:29:05Z",
|
||||
"lastUpdatedDate": "2025-01-28 06:29:05Z"
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"ea3b2bf4f5b3d0bd8a6ad35cc61e49f2a1596660fd66d17d740e4806e7ed7dcc": {
|
||||
"signature": "ea3b2bf4f5b3d0bd8a6ad35cc61e49f2a1596660fd66d17d740e4806e7ed7dcc",
|
||||
"alternativeSignatures": [
|
||||
"ff528c0b5a010ae7b5e9178b004a8b816a429a28ba98ce8336466b490a09dcef"
|
||||
],
|
||||
"target": ".build/win32-arm64/system-setup/VSCodeSetup-arm64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-01-30 19:19:49Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"12babbc85192ed1c8d927693da788537c1eef199bbecbe226f940a2d0e97637c": {
|
||||
"signature": "12babbc85192ed1c8d927693da788537c1eef199bbecbe226f940a2d0e97637c",
|
||||
"alternativeSignatures": [
|
||||
"35b0519e201e56fb87fc6fb085e6fb1df5b89715142bb9086a5b2006e0fd4ced"
|
||||
],
|
||||
"target": ".build/win32-arm64/system-setup/VSCodeSetup-arm64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-01-30 19:19:49Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"49163bd1dc9d965d3baced1694dc8c43305b8bf96e884f478d8e4bd124454ba0": {
|
||||
"signature": "49163bd1dc9d965d3baced1694dc8c43305b8bf96e884f478d8e4bd124454ba0",
|
||||
"alternativeSignatures": [
|
||||
"aa80bcf44aa8ddd20fb9802e9032c1257048b973896a944ded70bb195f060b2a"
|
||||
],
|
||||
"target": ".build/win32-arm64/user-setup/VSCodeUserSetup-arm64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-01-30 19:21:17Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"c405af02e021c3a473d4e45ec4daa658db1527ea7430c6be968d182e7b50fbd1": {
|
||||
"signature": "c405af02e021c3a473d4e45ec4daa658db1527ea7430c6be968d182e7b50fbd1",
|
||||
"alternativeSignatures": [
|
||||
"619d2a1a77f55b4181493b8cfdf09be5261e539115752af2e4938f5ac04af132"
|
||||
],
|
||||
"target": ".build/win32-arm64/user-setup/VSCodeUserSetup-arm64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-01-30 19:21:17Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"71b8515b2eb51cfd5eace11cedb15189d51ce9e479095a5938334416088cbc03": {
|
||||
"signature": "71b8515b2eb51cfd5eace11cedb15189d51ce9e479095a5938334416088cbc03",
|
||||
"alternativeSignatures": [
|
||||
"b34279fc5fec828b8dcd9ca873804e85d7d9cd78554ec109d2dd493351a7a244"
|
||||
],
|
||||
"target": ".build/win32-x64/system-setup/VSCodeSetup-x64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-01-30 19:51:51Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"9238de77a5320039def14694d1b6f501cc2288f13c9c688d2e0501fc5a56ee61": {
|
||||
"signature": "9238de77a5320039def14694d1b6f501cc2288f13c9c688d2e0501fc5a56ee61",
|
||||
"alternativeSignatures": [
|
||||
"1d17616a549e9f36d814c4e802d651b1af453ce0a23d4478eef39be81adcc16b"
|
||||
],
|
||||
"target": ".build/win32-x64/system-setup/VSCodeSetup-x64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-01-30 19:51:51Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"bad8b698b48c1da9ece953903581c66bf98bc829ae1a6adcd3b5c2056a6fcd01": {
|
||||
"signature": "bad8b698b48c1da9ece953903581c66bf98bc829ae1a6adcd3b5c2056a6fcd01",
|
||||
"alternativeSignatures": [
|
||||
"057376d31b97e8ce3ecf6a180a553b932d7e5be6e2b07a08027d5dfabe35e82c"
|
||||
],
|
||||
"target": ".build/win32-x64/user-setup/VSCodeUserSetup-x64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-01-30 19:53:13Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"cc7c248b0fd4c105e9a393ae232bf0d314ec50e65357a5e7e7d68f6f10c77077": {
|
||||
"signature": "cc7c248b0fd4c105e9a393ae232bf0d314ec50e65357a5e7e7d68f6f10c77077",
|
||||
"alternativeSignatures": [
|
||||
"f3867098aff3368682df9926e85a35ec05cf905f27d0c157430021c3169f899d"
|
||||
],
|
||||
"target": ".build/win32-x64/user-setup/VSCodeUserSetup-x64-1.97.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-01-30 19:53:13Z",
|
||||
"expirationDate": "2025-07-19 21:12:48Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-01-30 21:12:48Z"
|
||||
},
|
||||
"8c53250a171412b84dedcbb22cdab9ec365d9b52d74b09c070097fff45372de0": {
|
||||
"signature": "8c53250a171412b84dedcbb22cdab9ec365d9b52d74b09c070097fff45372de0",
|
||||
"alternativeSignatures": [
|
||||
"314267784b0ea867006e00b809a93498fae3264e42d1a3a7745ab13180a5b6ef"
|
||||
],
|
||||
"target": ".build/win32-arm64/system-setup/VSCodeSetup-arm64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-02-04 06:16:33Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"a6a58d971da858f4af219672cef73ffd0aacc47f1e2c12b8b44a428e1330d3de": {
|
||||
"signature": "a6a58d971da858f4af219672cef73ffd0aacc47f1e2c12b8b44a428e1330d3de",
|
||||
"alternativeSignatures": [
|
||||
"4e40f2f1683f0bf2245f35d0ebbcf2f446274d84b1db09d8e76ddfdcad5d4479"
|
||||
],
|
||||
"target": ".build/win32-arm64/system-setup/VSCodeSetup-arm64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-02-04 06:16:33Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"90e0f060e01e4a55620f609ac3241b62e8f54a059e9f4d292e93a4305fd3c39e": {
|
||||
"signature": "90e0f060e01e4a55620f609ac3241b62e8f54a059e9f4d292e93a4305fd3c39e",
|
||||
"alternativeSignatures": [
|
||||
"377fe43ff8404d07f4a6ca763175004f360397ded6cf5d55b655646ada90e39c"
|
||||
],
|
||||
"target": ".build/win32-arm64/user-setup/VSCodeUserSetup-arm64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-02-04 06:17:54Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"f36c3dc19566098a923877d16d6ebfcbd971f8fcd8210afb8f5558fb5ba1f203": {
|
||||
"signature": "f36c3dc19566098a923877d16d6ebfcbd971f8fcd8210afb8f5558fb5ba1f203",
|
||||
"alternativeSignatures": [
|
||||
"1af1f475c1617701e3d7a8fd465916bcc60c3125b8807af5d47d49137d9d468c"
|
||||
],
|
||||
"target": ".build/win32-arm64/user-setup/VSCodeUserSetup-arm64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-02-04 06:17:54Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"71193d108c53bb802f5c491276365bcff0645fb380be57288f3fbd6896166d3a": {
|
||||
"signature": "71193d108c53bb802f5c491276365bcff0645fb380be57288f3fbd6896166d3a",
|
||||
"alternativeSignatures": [
|
||||
"420cae2e6e34b93d7b74fc1ffddfdf23b57650ae989d838bb2d67f28e4e1db0e"
|
||||
],
|
||||
"target": ".build/win32-x64/system-setup/VSCodeSetup-x64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-02-04 07:11:19Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"444c302f49bdedcafe772322a09727b2279e3265d99deb2e307defeae3ef200b": {
|
||||
"signature": "444c302f49bdedcafe772322a09727b2279e3265d99deb2e307defeae3ef200b",
|
||||
"alternativeSignatures": [
|
||||
"4ff6ccbdb0745d43d3b61f82fb2f4d8a64fe9787525df81a6d7b825e79282085"
|
||||
],
|
||||
"target": ".build/win32-x64/system-setup/VSCodeSetup-x64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-02-04 07:11:19Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"4670c7c096a69ca428429ffa1f5250aac9f2e07beac0ffe587ffb37bdb1da4d4": {
|
||||
"signature": "4670c7c096a69ca428429ffa1f5250aac9f2e07beac0ffe587ffb37bdb1da4d4",
|
||||
"alternativeSignatures": [
|
||||
"7cead96cb508ab6e37e27bcc0f8b7ed8d0761b77f4793958c46c5ff3892ab1b6"
|
||||
],
|
||||
"target": ".build/win32-x64/user-setup/VSCodeUserSetup-x64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2009",
|
||||
"createdDate": "2025-02-04 07:13:22Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
},
|
||||
"a359b4a5ed2378a73f3bba93e3fb1c595db7423c3082635d12d101bbeb0a51b8": {
|
||||
"signature": "a359b4a5ed2378a73f3bba93e3fb1c595db7423c3082635d12d101bbeb0a51b8",
|
||||
"alternativeSignatures": [
|
||||
"125b52a21ef619a95e695085deb9492280bcf2c1decdd5e87e6416af5982d02d"
|
||||
],
|
||||
"target": ".build/win32-x64/user-setup/VSCodeUserSetup-x64-1.98.0-insider.exe",
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"tool": "binskim",
|
||||
"ruleId": "BA2018",
|
||||
"createdDate": "2025-02-04 07:13:22Z",
|
||||
"expirationDate": "2025-07-24 07:25:17Z",
|
||||
"justification": "This error is baselined with an expiration date of 180 days from 2025-02-04 07:25:17Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"hydrated": false,
|
||||
"properties": {
|
||||
"helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/suppressions"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"suppressionSets": {
|
||||
"default": {
|
||||
"name": "default",
|
||||
"createdDate": "2025-03-17 11:52:32Z",
|
||||
"lastUpdatedDate": "2025-03-17 11:52:32Z"
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8": {
|
||||
"signature": "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8",
|
||||
"alternativeSignatures": [
|
||||
"07746898f43afab7cc50931b33154c2d9e1a35f82a649dbe8aecf785b3d5a813"
|
||||
],
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"createdDate": "2025-03-17 11:52:32Z"
|
||||
},
|
||||
"77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624": {
|
||||
"signature": "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624",
|
||||
"alternativeSignatures": [
|
||||
"4a6cb67bd4b401e9669c13a2162660aaefc0a94a4122e5b50c198414db545672"
|
||||
],
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"createdDate": "2025-03-17 11:52:32Z"
|
||||
},
|
||||
"30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad": {
|
||||
"signature": "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad",
|
||||
"alternativeSignatures": [
|
||||
"b7b9eb974d7d3a4ae14df8695ca5a62592c8c9d20b7eda70a6535d50cbda3e7f"
|
||||
],
|
||||
"memberOf": [
|
||||
"default"
|
||||
],
|
||||
"createdDate": "2025-03-17 11:52:32Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/vscode/wiki/How-to-Contribute
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
directives:
|
||||
description: Install Git
|
||||
allowPrerelease: true
|
||||
settings:
|
||||
id: Git.Git
|
||||
source: winget
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: npm
|
||||
directives:
|
||||
description: Install NodeJS version 20
|
||||
allowPrerelease: true
|
||||
settings:
|
||||
id: OpenJS.NodeJS.LTS
|
||||
version: "20.14.0"
|
||||
source: winget
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
directives:
|
||||
description: Install Python 3.10
|
||||
allowPrerelease: true
|
||||
settings:
|
||||
id: Python.Python.3.10
|
||||
source: winget
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2022 (any edition is OK)
|
||||
allowPrerelease: true
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.2022.BuildTools
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
- vsPackage
|
||||
directives:
|
||||
description: Install required VS workloads
|
||||
allowPrerelease: true
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.BuildTools
|
||||
channelId: VisualStudio.17.Release
|
||||
includeRecommended: true
|
||||
components:
|
||||
- Microsoft.VisualStudio.Workload.VCTools
|
||||
- resource: NpmDsc/NpmInstall
|
||||
dependsOn:
|
||||
- npm
|
||||
directives:
|
||||
description: Install dependencies
|
||||
allowPrerelease: true
|
||||
settings:
|
||||
PackageDirectory: '${WinGetConfigRoot}\..\'
|
||||
configurationVersion: 0.2.0
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm
|
||||
|
||||
ADD install-vscode.sh /root/
|
||||
RUN /root/install-vscode.sh
|
||||
|
||||
RUN git config --system codespaces-theme.hide-status 1
|
||||
|
||||
USER node
|
||||
RUN npm install -g node-gyp
|
||||
RUN NPM_CACHE="$(npm config get cache)" && rm -rf "$NPM_CACHE" && ln -s /vscode-dev/npm-cache "$NPM_CACHE"
|
||||
RUN echo 'export DISPLAY="${DISPLAY:-:1}"' | tee -a ~/.bashrc >> ~/.zshrc
|
||||
|
||||
USER root
|
||||
CMD chown node:node /vscode-dev && sudo -u node mkdir -p /vscode-dev/npm-cache && sleep inf
|
||||
@@ -0,0 +1,112 @@
|
||||
# Code - OSS Development Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/vscode)
|
||||
|
||||
This repository includes configuration for a development container for working with Code - OSS in a local container or using [GitHub Codespaces](https://github.com/features/codespaces).
|
||||
|
||||
> **Tip:** The default VNC password is `vscode`. The VNC server runs on port `5901` and a web client is available on port `6080`.
|
||||
|
||||
## Quick start - local
|
||||
|
||||
If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/vscode) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
||||
|
||||
1. Install Docker Desktop or Docker for Linux on your local machine. (See [docs](https://aka.ms/vscode-remote/containers/getting-started) for additional details.)
|
||||
|
||||
2. **Important**: Docker needs at least **4 Cores and 8 GB of RAM** to run a full build with **9 GB of RAM** being recommended. If you are on macOS, or are using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item and going to **Preferences/Settings > Resources > Advanced**.
|
||||
|
||||
> **Note:** The [Resource Monitor](https://marketplace.visualstudio.com/items?itemName=mutantdino.resourcemonitor) extension is included in the container so you can keep an eye on CPU/Memory in the status bar.
|
||||
|
||||
3. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the [Dev Containers](https://aka.ms/vscode-remote/download/containers) extension.
|
||||
|
||||

|
||||
|
||||
> **Note:** The Dev Containers extension requires the Visual Studio Code distribution of Code - OSS. See the [FAQ](https://aka.ms/vscode-remote/faq/license) for details.
|
||||
|
||||
4. Press <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd> or <kbd>F1</kbd> and select **Dev Containers: Clone Repository in Container Volume...**.
|
||||
|
||||
> **Tip:** While you can use your local source tree instead, operations like `npm i` can be slow on macOS or when using the Hyper-V engine on Windows. We recommend using the WSL filesystem on Windows or the "clone repository in container" approach on Windows and macOS instead since it uses "named volume" rather than the local filesystem.
|
||||
|
||||
5. Type `https://github.com/microsoft/vscode` (or a branch or PR URL) in the input box and press <kbd>Enter</kbd>.
|
||||
|
||||
6. After the container is running:
|
||||
1. If you have the `DISPLAY` or `WAYLAND_DISPLAY` environment variables set locally (or in WSL on Windows), desktop apps in the container will be shown in local windows.
|
||||
2. If these are not set, open a web browser and go to [http://localhost:6080](http://localhost:6080), or use a [VNC Viewer][def] to connect to `localhost:5901` and enter `vscode` as the password. Anything you start in VS Code, or the integrated terminal, will appear here.
|
||||
|
||||
Next: **[Try it out!](#try-it)**
|
||||
|
||||
## Quick start - GitHub Codespaces
|
||||
|
||||
1. From the [microsoft/vscode GitHub repository](https://github.com/microsoft/vscode), click on the **Code** dropdown, select **Open with Codespaces**, and then click on **New codespace**. If prompted, select the **Standard** machine size (which is also the default).
|
||||
|
||||
> **Note:** You will not see these options within GitHub if you are not in the Codespaces beta.
|
||||
|
||||
2. After the codespace is up and running in your browser, press <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd> or <kbd>F1</kbd> and select **Ports: Focus on Ports View**.
|
||||
|
||||
3. You should see **VNC web client (6080)** under in the list of ports. Select the line and click on the globe icon to open it in a browser tab.
|
||||
|
||||
> **Tip:** If you do not see the port, <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd> or <kbd>F1</kbd>, select **Forward a Port** and enter port `6080`.
|
||||
|
||||
4. In the new tab, you should see noVNC. Click **Connect** and enter `vscode` as the password.
|
||||
|
||||
Anything you start in VS Code, or the integrated terminal, will appear here.
|
||||
|
||||
Next: **[Try it out!](#try-it)**
|
||||
|
||||
### Using VS Code with GitHub Codespaces
|
||||
|
||||
You may see improved VNC responsiveness when accessing a codespace from VS Code client since you can use a [VNC Viewer][def]. Here's how to do it.
|
||||
|
||||
1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces).
|
||||
|
||||
> **Note:** The GitHub Codespaces extension requires the Visual Studio Code distribution of Code - OSS.
|
||||
|
||||
2. After the VS Code is up and running, press <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd> or <kbd>F1</kbd>, choose **Codespaces: Create New Codespace**, and use the following settings:
|
||||
|
||||
- `microsoft/vscode` for the repository.
|
||||
- Select any branch (e.g. **main**) - you can select a different one later.
|
||||
- Choose **Standard** (4-core, 8GB) as the size.
|
||||
|
||||
3. After you have connected to the codespace, you can use a [VNC Viewer][def] to connect to `localhost:5901` and enter `vscode` as the password.
|
||||
|
||||
> **Tip:** You may also need change your VNC client's **Picture Quality** setting to **High** to get a full color desktop.
|
||||
|
||||
4. Anything you start in VS Code, or the integrated terminal, will appear here.
|
||||
|
||||
Next: **[Try it out!](#try-it)**
|
||||
|
||||
## Try it
|
||||
|
||||
This container uses the [Fluxbox](http://fluxbox.org/) window manager to keep things lean. **Right-click on the desktop** to see menu options. It works with GNOME and GTK applications, so other tools can be installed if needed.
|
||||
|
||||
> **Note:** You can also set the resolution from the command line by typing `set-resolution`.
|
||||
|
||||
To start working with Code - OSS, follow these steps:
|
||||
|
||||
1. In your local VS Code client, open a terminal (<kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>\`</kbd>) and type the following commands:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
bash scripts/code.sh
|
||||
```
|
||||
|
||||
2. After the build is complete, open a web browser or a [VNC Viewer][def] to connect to the desktop environment as described in the quick start and enter `vscode` as the password.
|
||||
|
||||
3. You should now see Code - OSS!
|
||||
|
||||
Next, let's try debugging.
|
||||
|
||||
1. Shut down Code - OSS by clicking the box in the upper right corner of the Code - OSS window through your browser or VNC viewer.
|
||||
|
||||
2. Go to your local VS Code client, and use the **Run / Debug** view to launch the **VS Code** configuration. (Typically the default, so you can likely just press <kbd>F5</kbd>).
|
||||
|
||||
> **Note:** If launching times out, you can increase the value of `timeout` in the "VS Code", "Attach Main Process", "Attach Extension Host", and "Attach to Shared Process" configurations in [launch.json](../../.vscode/launch.json). However, running `./scripts/code.sh` first will set up Electron which will usually solve timeout issues.
|
||||
|
||||
3. After a bit, Code - OSS will appear with the debugger attached!
|
||||
|
||||
Enjoy!
|
||||
|
||||
### Notes
|
||||
|
||||
The container comes with VS Code Insiders installed. To run it from an Integrated Terminal use `VSCODE_IPC_HOOK_CLI= /usr/bin/code-insiders .`.
|
||||
|
||||
[def]: https://www.realvnc.com/en/connect/download/viewer/
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/desktop-lite:1": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "ghcr.io/devcontainers/features/desktop-lite@sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7",
|
||||
"integrity": "sha256:e7dc4d37ab9e3d6e7ebb221bac741f5bfe07dae47025399d038b17af2ed8ddb7"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "ghcr.io/devcontainers/features/rust@sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb",
|
||||
"integrity": "sha256:aba6f47303b197976902bf544c786b5efecc03c238ff593583e5e74dfa9c7ccb"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "Code - OSS",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/desktop-lite:1": {},
|
||||
"ghcr.io/devcontainers/features/rust:1": {}
|
||||
},
|
||||
"containerEnv": {
|
||||
"DISPLAY": "" // Allow the Dev Containers extension to set DISPLAY, post-create.sh will add it back in ~/.bashrc and ~/.zshrc if not set.
|
||||
},
|
||||
"overrideCommand": false,
|
||||
"privileged": true,
|
||||
"mounts": [
|
||||
{
|
||||
"source": "vscode-dev",
|
||||
"target": "/vscode-dev",
|
||||
"type": "volume"
|
||||
}
|
||||
],
|
||||
"postCreateCommand": "./.devcontainer/post-create.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"resmon.show.battery": false,
|
||||
"resmon.show.cpufreq": false
|
||||
},
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"ms-vscode.vscode-github-issue-notebooks",
|
||||
"ms-vscode.vscode-selfhost-test-provider",
|
||||
"mutantdino.resourcemonitor"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [6080, 5901],
|
||||
"portsAttributes": {
|
||||
"6080": {
|
||||
"label": "VNC web client (noVNC)",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"5901": {
|
||||
"label": "VNC TCP port",
|
||||
"onAutoForward": "silent"
|
||||
}
|
||||
},
|
||||
"hostRequirements": {
|
||||
"memory": "9gb"
|
||||
}
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
apt update
|
||||
apt install -y wget gpg
|
||||
|
||||
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
|
||||
install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg
|
||||
sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
|
||||
rm -f packages.microsoft.gpg
|
||||
|
||||
apt update
|
||||
apt install -y code-insiders libsecret-1-dev libxkbfile-dev libkrb5-dev
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
npm i
|
||||
npm run electron
|
||||
@@ -0,0 +1,15 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Tab indentation
|
||||
[*]
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# The indent size used in the `package.json` file cannot be changed
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
[{*.yml,*.yaml,package.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,41 @@
|
||||
**/build/*/**/*.js
|
||||
**/dist/**/*.js
|
||||
**/extensions/**/*.d.ts
|
||||
**/extensions/**/build/**
|
||||
**/extensions/**/colorize-fixtures/**
|
||||
**/extensions/css-language-features/server/test/pathCompletionFixtures/**
|
||||
**/extensions/html-language-features/server/lib/jquery.d.ts
|
||||
**/extensions/html-language-features/server/src/test/pathCompletionFixtures/**
|
||||
**/extensions/ipynb/notebook-out/**
|
||||
**/extensions/markdown-language-features/media/**
|
||||
**/extensions/markdown-language-features/notebook-out/**
|
||||
**/extensions/markdown-math/notebook-out/**
|
||||
**/extensions/notebook-renderers/renderer-out/index.js
|
||||
**/extensions/simple-browser/media/index.js
|
||||
**/extensions/terminal-suggest/src/completions/upstream/**
|
||||
**/extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts
|
||||
**/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts
|
||||
**/extensions/terminal-suggest/third_party/**
|
||||
**/extensions/typescript-language-features/test-workspace/**
|
||||
**/extensions/typescript-language-features/extension.webpack.config.js
|
||||
**/extensions/typescript-language-features/extension-browser.webpack.config.js
|
||||
**/extensions/typescript-language-features/package-manager/node-maintainer/**
|
||||
**/extensions/vscode-api-tests/testWorkspace/**
|
||||
**/extensions/vscode-api-tests/testWorkspace2/**
|
||||
**/fixtures/**
|
||||
**/node_modules/**
|
||||
**/out-*/**/*.js
|
||||
**/out-editor-*/**
|
||||
**/out/**/*.js
|
||||
**/src/**/dompurify.js
|
||||
**/src/**/marked.js
|
||||
**/src/**/semver.js
|
||||
**/src/typings/**/*.d.ts
|
||||
**/src/vs/*/**/*.d.ts
|
||||
**/src/vs/base/test/common/filters.perf.data.js
|
||||
**/src/vs/loader.js
|
||||
**/test/unit/assert.js
|
||||
**/test/automation/out/**
|
||||
**/typings/**
|
||||
**/.build/**
|
||||
!.vscode
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { join } from 'path';
|
||||
|
||||
|
||||
export = new class ApiProviderNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
amdX: 'Use `import type` for import declarations, use `amdX#importAMDNodeModule` for import expressions'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const modules = new Set<string>();
|
||||
|
||||
try {
|
||||
const { dependencies, optionalDependencies } = require(join(__dirname, '../package.json'));
|
||||
const all = Object.keys(dependencies).concat(Object.keys(optionalDependencies));
|
||||
for (const key of all) {
|
||||
modules.add(key);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
const checkImport = (node: any) => {
|
||||
|
||||
if (node.type !== 'Literal' || typeof node.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent.importKind === 'type') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modules.has(node.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'amdX'
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
['ImportExpression Literal']: checkImport,
|
||||
['ImportDeclaration Literal']: checkImport
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class DeclareServiceBrand implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
fixable: 'code',
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return {
|
||||
['PropertyDefinition[key.name="_serviceBrand"][value]']: (node: any) => {
|
||||
return context.report({
|
||||
node,
|
||||
message: `The '_serviceBrand'-property should not have a value`,
|
||||
fix: (fixer) => {
|
||||
return fixer.replaceText(node, 'declare _serviceBrand: undefined;');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { Node } from 'estree';
|
||||
|
||||
export = new class EnsureNoDisposablesAreLeakedInTestSuite implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
ensure: 'Suites should include a call to `ensureNoDisposablesAreLeakedInTestSuite()` to ensure no disposables are leaked in tests.'
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
const config = <{ exclude: string[] }>context.options[0];
|
||||
|
||||
const needle = context.getFilename().replace(/\\/g, '/');
|
||||
if (config.exclude.some((e) => needle.endsWith(e))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
[`Program > ExpressionStatement > CallExpression[callee.name='suite']`]: (node: Node) => {
|
||||
const src = context.getSourceCode().getText(node);
|
||||
if (!src.includes('ensureNoDisposablesAreLeakedInTestSuite(')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'ensure',
|
||||
fix: (fixer) => {
|
||||
const updatedSrc = src.replace(/(suite\(.*\n)/, '$1\n\tensureNoDisposablesAreLeakedInTestSuite();\n');
|
||||
return fixer.replaceText(node, updatedSrc);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,287 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import * as path from 'path';
|
||||
import minimatch from 'minimatch';
|
||||
import { createImportRuleListener } from './utils';
|
||||
|
||||
const REPO_ROOT = path.normalize(path.join(__dirname, '../'));
|
||||
|
||||
interface ConditionalPattern {
|
||||
when?: 'hasBrowser' | 'hasNode' | 'hasElectron' | 'test';
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
interface RawImportPatternsConfig {
|
||||
target: string;
|
||||
layer?: 'common' | 'worker' | 'browser' | 'electron-sandbox' | 'node' | 'electron-utility' | 'electron-main';
|
||||
test?: boolean;
|
||||
restrictions: string | (string | ConditionalPattern)[];
|
||||
}
|
||||
|
||||
interface LayerAllowRule {
|
||||
when: 'hasBrowser' | 'hasNode' | 'hasElectron' | 'test';
|
||||
allow: string[];
|
||||
}
|
||||
|
||||
type RawOption = RawImportPatternsConfig | LayerAllowRule;
|
||||
|
||||
function isLayerAllowRule(option: RawOption): option is LayerAllowRule {
|
||||
return !!((<LayerAllowRule>option).when && (<LayerAllowRule>option).allow);
|
||||
}
|
||||
|
||||
interface ImportPatternsConfig {
|
||||
target: string;
|
||||
restrictions: string[];
|
||||
}
|
||||
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
badImport: 'Imports violates \'{{restrictions}}\' restrictions. See https://github.com/microsoft/vscode/wiki/Source-Code-Organization',
|
||||
badFilename: 'Missing definition in `code-import-patterns` for this file. Define rules at https://github.com/microsoft/vscode/blob/main/eslint.config.js',
|
||||
badAbsolute: 'Imports have to be relative to support ESM',
|
||||
badExtension: 'Imports have to end with `.js` or `.css` to support ESM',
|
||||
},
|
||||
docs: {
|
||||
url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
const options = <RawOption[]>context.options;
|
||||
const configs = this._processOptions(options);
|
||||
const relativeFilename = getRelativeFilename(context);
|
||||
|
||||
for (const config of configs) {
|
||||
if (minimatch(relativeFilename, config.target)) {
|
||||
return createImportRuleListener((node, value) => this._checkImport(context, config, node, value));
|
||||
}
|
||||
}
|
||||
|
||||
context.report({
|
||||
loc: { line: 1, column: 0 },
|
||||
messageId: 'badFilename'
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private _optionsCache = new WeakMap<RawOption[], ImportPatternsConfig[]>();
|
||||
|
||||
private _processOptions(options: RawOption[]): ImportPatternsConfig[] {
|
||||
if (this._optionsCache.has(options)) {
|
||||
return this._optionsCache.get(options)!;
|
||||
}
|
||||
|
||||
type Layer = 'common' | 'worker' | 'browser' | 'electron-sandbox' | 'node' | 'electron-utility' | 'electron-main';
|
||||
|
||||
interface ILayerRule {
|
||||
layer: Layer;
|
||||
deps: string;
|
||||
isBrowser?: boolean;
|
||||
isNode?: boolean;
|
||||
isElectron?: boolean;
|
||||
}
|
||||
|
||||
function orSegment(variants: Layer[]): string {
|
||||
return (variants.length === 1 ? variants[0] : `{${variants.join(',')}}`);
|
||||
}
|
||||
|
||||
const layerRules: ILayerRule[] = [
|
||||
{ layer: 'common', deps: orSegment(['common']) },
|
||||
{ layer: 'worker', deps: orSegment(['common', 'worker']) },
|
||||
{ layer: 'browser', deps: orSegment(['common', 'browser']), isBrowser: true },
|
||||
{ layer: 'electron-sandbox', deps: orSegment(['common', 'browser', 'electron-sandbox']), isBrowser: true },
|
||||
{ layer: 'node', deps: orSegment(['common', 'node']), isNode: true },
|
||||
{ layer: 'electron-utility', deps: orSegment(['common', 'node', 'electron-utility']), isNode: true, isElectron: true },
|
||||
{ layer: 'electron-main', deps: orSegment(['common', 'node', 'electron-utility', 'electron-main']), isNode: true, isElectron: true },
|
||||
];
|
||||
|
||||
let browserAllow: string[] = [];
|
||||
let nodeAllow: string[] = [];
|
||||
let electronAllow: string[] = [];
|
||||
let testAllow: string[] = [];
|
||||
for (const option of options) {
|
||||
if (isLayerAllowRule(option)) {
|
||||
if (option.when === 'hasBrowser') {
|
||||
browserAllow = option.allow.slice(0);
|
||||
} else if (option.when === 'hasNode') {
|
||||
nodeAllow = option.allow.slice(0);
|
||||
} else if (option.when === 'hasElectron') {
|
||||
electronAllow = option.allow.slice(0);
|
||||
} else if (option.when === 'test') {
|
||||
testAllow = option.allow.slice(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findLayer(layer: Layer): ILayerRule | null {
|
||||
for (const layerRule of layerRules) {
|
||||
if (layerRule.layer === layer) {
|
||||
return layerRule;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateConfig(layerRule: ILayerRule, target: string, rawRestrictions: (string | ConditionalPattern)[]): [ImportPatternsConfig, ImportPatternsConfig] {
|
||||
const restrictions: string[] = [];
|
||||
const testRestrictions: string[] = [...testAllow];
|
||||
|
||||
if (layerRule.isBrowser) {
|
||||
restrictions.push(...browserAllow);
|
||||
}
|
||||
|
||||
if (layerRule.isNode) {
|
||||
restrictions.push(...nodeAllow);
|
||||
}
|
||||
|
||||
if (layerRule.isElectron) {
|
||||
restrictions.push(...electronAllow);
|
||||
}
|
||||
|
||||
for (const rawRestriction of rawRestrictions) {
|
||||
let importPattern: string;
|
||||
let when: 'hasBrowser' | 'hasNode' | 'hasElectron' | 'test' | undefined = undefined;
|
||||
if (typeof rawRestriction === 'string') {
|
||||
importPattern = rawRestriction;
|
||||
} else {
|
||||
importPattern = rawRestriction.pattern;
|
||||
when = rawRestriction.when;
|
||||
}
|
||||
if (typeof when === 'undefined'
|
||||
|| (when === 'hasBrowser' && layerRule.isBrowser)
|
||||
|| (when === 'hasNode' && layerRule.isNode)
|
||||
|| (when === 'hasElectron' && layerRule.isElectron)
|
||||
) {
|
||||
restrictions.push(importPattern.replace(/\/\~$/, `/${layerRule.deps}/**`));
|
||||
testRestrictions.push(importPattern.replace(/\/\~$/, `/test/${layerRule.deps}/**`));
|
||||
} else if (when === 'test') {
|
||||
testRestrictions.push(importPattern.replace(/\/\~$/, `/${layerRule.deps}/**`));
|
||||
testRestrictions.push(importPattern.replace(/\/\~$/, `/test/${layerRule.deps}/**`));
|
||||
}
|
||||
}
|
||||
|
||||
testRestrictions.push(...restrictions);
|
||||
|
||||
return [
|
||||
{
|
||||
target: target.replace(/\/\~$/, `/${layerRule.layer}/**`),
|
||||
restrictions: restrictions
|
||||
},
|
||||
{
|
||||
target: target.replace(/\/\~$/, `/test/${layerRule.layer}/**`),
|
||||
restrictions: testRestrictions
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const configs: ImportPatternsConfig[] = [];
|
||||
for (const option of options) {
|
||||
if (isLayerAllowRule(option)) {
|
||||
continue;
|
||||
}
|
||||
const target = option.target;
|
||||
const targetIsVS = /^src\/vs\//.test(target);
|
||||
const restrictions = (typeof option.restrictions === 'string' ? [option.restrictions] : option.restrictions).slice(0);
|
||||
|
||||
if (targetIsVS) {
|
||||
// Always add "vs/nls" and "vs/amdX"
|
||||
restrictions.push('vs/nls.js');
|
||||
restrictions.push('vs/amdX.js'); // TODO@jrieken remove after ESM is real
|
||||
}
|
||||
|
||||
if (targetIsVS && option.layer) {
|
||||
// single layer => simple substitution for /~
|
||||
const layerRule = findLayer(option.layer);
|
||||
if (layerRule) {
|
||||
const [config, testConfig] = generateConfig(layerRule, target, restrictions);
|
||||
if (option.test) {
|
||||
configs.push(testConfig);
|
||||
} else {
|
||||
configs.push(config);
|
||||
}
|
||||
}
|
||||
} else if (targetIsVS && /\/\~$/.test(target)) {
|
||||
// generate all layers
|
||||
for (const layerRule of layerRules) {
|
||||
const [config, testConfig] = generateConfig(layerRule, target, restrictions);
|
||||
configs.push(config);
|
||||
configs.push(testConfig);
|
||||
}
|
||||
} else {
|
||||
configs.push({ target, restrictions: <string[]>restrictions.filter(r => typeof r === 'string') });
|
||||
}
|
||||
}
|
||||
this._optionsCache.set(options, configs);
|
||||
return configs;
|
||||
}
|
||||
|
||||
private _checkImport(context: eslint.Rule.RuleContext, config: ImportPatternsConfig, node: TSESTree.Node, importPath: string) {
|
||||
const targetIsVS = /^src\/vs\//.test(getRelativeFilename(context));
|
||||
if (targetIsVS) {
|
||||
|
||||
// ESM: check for import ending with ".js" or ".css"
|
||||
if (importPath[0] === '.' && !importPath.endsWith('.js') && !importPath.endsWith('.css')) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'badExtension',
|
||||
});
|
||||
}
|
||||
|
||||
// check for import being relative
|
||||
if (importPath.startsWith('vs/')) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'badAbsolute',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// resolve relative paths
|
||||
if (importPath[0] === '.') {
|
||||
const relativeFilename = getRelativeFilename(context);
|
||||
importPath = path.posix.join(path.posix.dirname(relativeFilename), importPath);
|
||||
if (/^src\/vs\//.test(importPath)) {
|
||||
// resolve using base url
|
||||
importPath = importPath.substring('src/'.length);
|
||||
}
|
||||
}
|
||||
|
||||
const restrictions = config.restrictions;
|
||||
|
||||
let matched = false;
|
||||
for (const pattern of restrictions) {
|
||||
if (minimatch(importPath, pattern)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
// None of the restrictions matched
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'badImport',
|
||||
data: {
|
||||
restrictions: restrictions.join(' or ')
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the filename relative to the project root and using `/` as separators
|
||||
*/
|
||||
function getRelativeFilename(context: eslint.Rule.RuleContext): string {
|
||||
const filename = path.normalize(context.getFilename());
|
||||
return filename.substring(REPO_ROOT.length).replace(/\\/g, '/');
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { join, dirname } from 'path';
|
||||
import { createImportRuleListener } from './utils';
|
||||
|
||||
type Config = {
|
||||
allowed: Set<string>;
|
||||
disallowed: Set<string>;
|
||||
};
|
||||
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
layerbreaker: 'Bad layering. You are not allowed to access {{from}} from here, allowed layers are: [{{allowed}}]'
|
||||
},
|
||||
docs: {
|
||||
url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization'
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const fileDirname = dirname(context.getFilename());
|
||||
const parts = fileDirname.split(/\\|\//);
|
||||
const ruleArgs = <Record<string, string[]>>context.options[0];
|
||||
|
||||
let config: Config | undefined;
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
if (ruleArgs[parts[i]]) {
|
||||
config = {
|
||||
allowed: new Set(ruleArgs[parts[i]]).add(parts[i]),
|
||||
disallowed: new Set()
|
||||
};
|
||||
Object.keys(ruleArgs).forEach(key => {
|
||||
if (!config!.allowed.has(key)) {
|
||||
config!.disallowed.add(key);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
// nothing
|
||||
return {};
|
||||
}
|
||||
|
||||
return createImportRuleListener((node, path) => {
|
||||
if (path[0] === '.') {
|
||||
path = join(dirname(context.getFilename()), path);
|
||||
}
|
||||
|
||||
const parts = dirname(path).split(/\\|\//);
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const part = parts[i];
|
||||
|
||||
if (config!.allowed.has(part)) {
|
||||
// GOOD - same layer
|
||||
break;
|
||||
}
|
||||
|
||||
if (config!.disallowed.has(part)) {
|
||||
// BAD - wrong layer
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'layerbreaker',
|
||||
data: {
|
||||
from: part,
|
||||
allowed: [...config!.allowed.keys()].join(', ')
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { dirname, relative } from 'path';
|
||||
import minimatch from 'minimatch';
|
||||
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
layerbreaker: 'You are only allowed to define limited top level functions.'
|
||||
},
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
let fileRelativePath = relative(dirname(__dirname), context.getFilename());
|
||||
if (!fileRelativePath.endsWith('/')) {
|
||||
fileRelativePath += '/';
|
||||
}
|
||||
const ruleArgs = <Record<string, string[]>>context.options[0];
|
||||
|
||||
const matchingKey = Object.keys(ruleArgs).find(key => fileRelativePath.startsWith(key) || minimatch(fileRelativePath, key));
|
||||
if (!matchingKey) {
|
||||
// nothing
|
||||
return {};
|
||||
}
|
||||
|
||||
const restrictedFunctions = ruleArgs[matchingKey];
|
||||
|
||||
return {
|
||||
FunctionDeclaration: (node: any) => {
|
||||
const isTopLevel = node.parent.type === 'Program';
|
||||
const functionName = node.id.name;
|
||||
if (isTopLevel && !restrictedFunctions.includes(node.id.name)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Top-level function '${functionName}' is restricted in this file. Allowed functions are: ${restrictedFunctions.join(', ')}.`
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration(node: any) {
|
||||
if (node.declaration && node.declaration.type === 'FunctionDeclaration') {
|
||||
const functionName = node.declaration.id.name;
|
||||
const isTopLevel = node.parent.type === 'Program';
|
||||
if (isTopLevel && !restrictedFunctions.includes(node.declaration.id.name)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Top-level function '${functionName}' is restricted in this file. Allowed functions are: ${restrictedFunctions.join(', ')}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
const VALID_USES = new Set<TSESTree.AST_NODE_TYPES | undefined>([
|
||||
TSESTree.AST_NODE_TYPES.AwaitExpression,
|
||||
TSESTree.AST_NODE_TYPES.VariableDeclarator,
|
||||
]);
|
||||
|
||||
export = new class MustUseResults implements eslint.Rule.RuleModule {
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
schema: false
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const config = <{ message: string; functions: string[] }[]>context.options[0];
|
||||
const listener: eslint.Rule.RuleListener = {};
|
||||
|
||||
for (const { message, functions } of config) {
|
||||
for (const fn of functions) {
|
||||
const query = `CallExpression[callee.property.name='${fn}'], CallExpression[callee.name='${fn}']`;
|
||||
listener[query] = (node: any) => {
|
||||
const cast: TSESTree.CallExpression = node;
|
||||
if (!VALID_USES.has(cast.parent?.type)) {
|
||||
context.report({ node, message });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return listener;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class NoAsyncSuite implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
function doesCallSuperDispose(node: any) {
|
||||
|
||||
if (!node.override) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = context.getSourceCode().getText(node);
|
||||
|
||||
if (body.includes('super.dispose')) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: 'dispose() should call super.dispose()'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
['MethodDefinition[override][key.name="dispose"]']: doesCallSuperDispose,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class NoDangerousTypeAssertions implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
// Disable in tests for now
|
||||
if (context.getFilename().includes('.test')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
// Disallow type assertions on object literals: <T>{ ... } or {} as T
|
||||
['TSTypeAssertion > ObjectExpression, TSAsExpression > ObjectExpression']: (node: any) => {
|
||||
const objectNode = node as TSESTree.Node;
|
||||
|
||||
const parent = objectNode.parent as TSESTree.TSTypeAssertion | TSESTree.TSAsExpression;
|
||||
if (
|
||||
// Allow `as const` assertions
|
||||
(parent.typeAnnotation.type === 'TSTypeReference' && parent.typeAnnotation.typeName.type === 'Identifier' && parent.typeAnnotation.typeName.name === 'const')
|
||||
|
||||
// For also now still allow `any` casts
|
||||
|| (parent.typeAnnotation.type === 'TSAnyKeyword')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: `Don't use type assertions for creating objects as this can hide type errors.`
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class NoGlobalDocumentListener implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return {
|
||||
CallExpression(node: any) {
|
||||
if (
|
||||
(
|
||||
node.callee.name === 'addDisposableListener' ||
|
||||
node.callee.property?.name === 'addDisposableListener'
|
||||
) &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === 'Identifier' &&
|
||||
node.arguments[0].name === 'document'
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use <targetWindow>.document to support multi-window scenarios. Resolve targetWindow with DOM.getWindow(element) or DOM.getActiveWindow() or use the predefined mainWindow constant.',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class ApiProviderNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
slow: 'Native private fields are much slower and should only be used when needed. Ignore this warning if you know what you are doing, use compile-time private otherwise. See https://github.com/microsoft/vscode/issues/185991#issuecomment-1614468158 for details',
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
return {
|
||||
['PropertyDefinition PrivateIdentifier']: (node: any) => {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'slow'
|
||||
});
|
||||
},
|
||||
['MethodDefinition PrivateIdentifier']: (node: any) => {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'slow'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { join } from 'path';
|
||||
import { createImportRuleListener } from './utils';
|
||||
|
||||
export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
noNls: 'Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const fileName = context.getFilename();
|
||||
if (
|
||||
/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.api/.test(fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.main/.test(fileName)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.worker.start/.test(fileName)
|
||||
) {
|
||||
return createImportRuleListener((node, path) => {
|
||||
// resolve relative paths
|
||||
if (path[0] === '.') {
|
||||
path = join(context.getFilename(), path);
|
||||
}
|
||||
|
||||
if (
|
||||
/vs(\/|\\)nls/.test(path)
|
||||
) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'noNls'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
/**
|
||||
* Checks for potentially unsafe usage of `DisposableStore` / `MutableDisposable`.
|
||||
*
|
||||
* These have been the source of leaks in the past.
|
||||
*/
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
function checkVariableDeclaration(inNode: any) {
|
||||
context.report({
|
||||
node: inNode,
|
||||
message: `Use const for 'DisposableStore' to avoid leaks by accidental reassignment.`
|
||||
});
|
||||
}
|
||||
|
||||
function checkProperty(inNode: any) {
|
||||
context.report({
|
||||
node: inNode,
|
||||
message: `Use readonly for DisposableStore/MutableDisposable to avoid leaks through accidental reassignment.`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'VariableDeclaration[kind!="const"] NewExpression[callee.name="DisposableStore"]': checkVariableDeclaration,
|
||||
|
||||
'PropertyDefinition[readonly!=true][typeAnnotation.typeAnnotation.typeName.name=/DisposableStore|MutableDisposable/]': checkProperty,
|
||||
'PropertyDefinition[readonly!=true] NewExpression[callee.name=/DisposableStore|MutableDisposable/]': checkProperty,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
||||
import * as eslint from 'eslint';
|
||||
import { dirname, join, relative } from 'path';
|
||||
import minimatch from 'minimatch';
|
||||
import { createImportRuleListener } from './utils';
|
||||
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
layerbreaker: 'You are only allowed to import {{import}} from here using `import type ...`.'
|
||||
},
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
let fileRelativePath = relative(dirname(__dirname), context.getFilename());
|
||||
if (!fileRelativePath.endsWith('/')) {
|
||||
fileRelativePath += '/';
|
||||
}
|
||||
const ruleArgs = <Record<string, string[]>>context.options[0];
|
||||
|
||||
const matchingKey = Object.keys(ruleArgs).find(key => fileRelativePath.startsWith(key) || minimatch(fileRelativePath, key));
|
||||
if (!matchingKey) {
|
||||
// nothing
|
||||
return {};
|
||||
}
|
||||
|
||||
const restrictedImports = ruleArgs[matchingKey];
|
||||
return createImportRuleListener((node, path) => {
|
||||
if (path[0] === '.') {
|
||||
path = join(dirname(context.getFilename()), path);
|
||||
}
|
||||
|
||||
if ((
|
||||
restrictedImports.includes(path) || restrictedImports.some(restriction => minimatch(path, restriction))
|
||||
) && !(
|
||||
(node.parent?.type === TSESTree.AST_NODE_TYPES.ImportDeclaration && node.parent.importKind === 'type') ||
|
||||
(node.parent && 'exportKind' in node.parent && node.parent.exportKind === 'type'))) { // the export could be multiple types
|
||||
context.report({
|
||||
loc: node.parent!.loc,
|
||||
messageId: 'layerbreaker',
|
||||
data: {
|
||||
import: path
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { join } from 'path';
|
||||
import { createImportRuleListener } from './utils';
|
||||
|
||||
export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
badImport: 'Not allowed to import standalone editor modules.'
|
||||
},
|
||||
docs: {
|
||||
url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
if (/vs(\/|\\)editor/.test(context.getFilename())) {
|
||||
// the vs/editor folder is allowed to use the standalone editor
|
||||
return {};
|
||||
}
|
||||
|
||||
return createImportRuleListener((node, path) => {
|
||||
|
||||
// resolve relative paths
|
||||
if (path[0] === '.') {
|
||||
path = join(context.getFilename(), path);
|
||||
}
|
||||
|
||||
if (
|
||||
/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.api/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.main/.test(path)
|
||||
|| /vs(\/|\\)editor(\/|\\)editor.worker.start/.test(path)
|
||||
) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'badImport'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
/**
|
||||
* WORKAROUND for https://github.com/evanw/esbuild/issues/3823
|
||||
*/
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
function checkProperty(inNode: any) {
|
||||
|
||||
const classDeclaration = context.sourceCode.getAncestors(inNode).find(node => node.type === 'ClassDeclaration');
|
||||
const propertyDefinition = <TSESTree.PropertyDefinition>inNode;
|
||||
|
||||
if (!classDeclaration || !classDeclaration.id?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!propertyDefinition.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const classCtor = classDeclaration.body.body.find(node => node.type === 'MethodDefinition' && node.kind === 'constructor');
|
||||
|
||||
if (!classCtor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = classDeclaration.id.name;
|
||||
const valueText = context.sourceCode.getText(<any>propertyDefinition.value);
|
||||
|
||||
if (valueText.includes(name + '.')) {
|
||||
|
||||
if (classCtor.value?.type === 'FunctionExpression' && !classCtor.value.params.find((param: any) => param.type === 'TSParameterProperty' && param.decorators?.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
loc: propertyDefinition.value.loc,
|
||||
message: `Static properties in decorated classes should not reference the class they are defined in. Use 'this' instead. This is a workaround for https://github.com/evanw/esbuild/issues/3823.`
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
'PropertyDefinition[static=true]': checkProperty,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
function isCallExpression(node: TSESTree.Node): node is TSESTree.CallExpression {
|
||||
return node.type === 'CallExpression';
|
||||
}
|
||||
|
||||
function isFunctionExpression(node: TSESTree.Node): node is TSESTree.FunctionExpression {
|
||||
return node.type.includes('FunctionExpression');
|
||||
}
|
||||
|
||||
export = new class NoAsyncSuite implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
function hasAsyncSuite(node: any) {
|
||||
if (isCallExpression(node) && node.arguments.length >= 2 && isFunctionExpression(node.arguments[1]) && node.arguments[1].async) {
|
||||
return context.report({
|
||||
node: node as any,
|
||||
message: 'suite factory function should never be async'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
['CallExpression[callee.name=/suite$/][arguments]']: hasAsyncSuite,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class NoTestOnly implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return {
|
||||
['MemberExpression[object.name=/^(test|suite)$/][property.name="only"]']: (node: any) => {
|
||||
return context.report({
|
||||
node,
|
||||
message: 'only is a dev-time tool and CANNOT be pushed'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
|
||||
function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral {
|
||||
return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
|
||||
}
|
||||
|
||||
function isDoubleQuoted(node: TSESTree.StringLiteral): boolean {
|
||||
return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"';
|
||||
}
|
||||
|
||||
export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule {
|
||||
|
||||
private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/;
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
doubleQuoted: 'Only use double-quoted strings for externalized strings.',
|
||||
badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.',
|
||||
duplicateKey: 'Duplicate key \'{{key}}\' with different message value.',
|
||||
badMessage: 'Message argument to \'{{message}}\' must be a string literal.'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const externalizedStringLiterals = new Map<string, { call: TSESTree.CallExpression; message: TSESTree.Node }[]>();
|
||||
const doubleQuotedStringLiterals = new Set<TSESTree.Node>();
|
||||
|
||||
function collectDoubleQuotedStrings(node: TSESTree.Literal) {
|
||||
if (isStringLiteral(node) && isDoubleQuoted(node)) {
|
||||
doubleQuotedStringLiterals.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
function visitLocalizeCall(node: TSESTree.CallExpression) {
|
||||
|
||||
// localize(key, message)
|
||||
const [keyNode, messageNode] = (<TSESTree.CallExpression>node).arguments;
|
||||
|
||||
// (1)
|
||||
// extract key so that it can be checked later
|
||||
let key: string | undefined;
|
||||
if (isStringLiteral(keyNode)) {
|
||||
doubleQuotedStringLiterals.delete(keyNode);
|
||||
key = keyNode.value;
|
||||
|
||||
} else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) {
|
||||
for (const property of keyNode.properties) {
|
||||
if (property.type === AST_NODE_TYPES.Property && !property.computed) {
|
||||
if (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'key') {
|
||||
if (isStringLiteral(property.value)) {
|
||||
doubleQuotedStringLiterals.delete(property.value);
|
||||
key = property.value.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof key === 'string') {
|
||||
let array = externalizedStringLiterals.get(key);
|
||||
if (!array) {
|
||||
array = [];
|
||||
externalizedStringLiterals.set(key, array);
|
||||
}
|
||||
array.push({ call: node, message: messageNode });
|
||||
}
|
||||
|
||||
// (2)
|
||||
// remove message-argument from doubleQuoted list and make
|
||||
// sure it is a string-literal
|
||||
doubleQuotedStringLiterals.delete(messageNode);
|
||||
if (!isStringLiteral(messageNode)) {
|
||||
context.report({
|
||||
loc: messageNode.loc,
|
||||
messageId: 'badMessage',
|
||||
data: { message: context.getSourceCode().getText(<any>node) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function visitL10NCall(node: TSESTree.CallExpression) {
|
||||
|
||||
// localize(key, message)
|
||||
const [messageNode] = (<TSESTree.CallExpression>node).arguments;
|
||||
|
||||
// remove message-argument from doubleQuoted list and make
|
||||
// sure it is a string-literal
|
||||
if (isStringLiteral(messageNode)) {
|
||||
doubleQuotedStringLiterals.delete(messageNode);
|
||||
} else if (messageNode.type === AST_NODE_TYPES.ObjectExpression) {
|
||||
for (const prop of messageNode.properties) {
|
||||
if (prop.type === AST_NODE_TYPES.Property) {
|
||||
if (prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === 'message') {
|
||||
doubleQuotedStringLiterals.delete(prop.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reportBadStringsAndBadKeys() {
|
||||
// (1)
|
||||
// report all strings that are in double quotes
|
||||
for (const node of doubleQuotedStringLiterals) {
|
||||
context.report({ loc: node.loc, messageId: 'doubleQuoted' });
|
||||
}
|
||||
|
||||
for (const [key, values] of externalizedStringLiterals) {
|
||||
|
||||
// (2)
|
||||
// report all invalid NLS keys
|
||||
if (!key.match(NoUnexternalizedStrings._rNlsKeys)) {
|
||||
for (const value of values) {
|
||||
context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } });
|
||||
}
|
||||
}
|
||||
|
||||
// (2)
|
||||
// report all invalid duplicates (same key, different message)
|
||||
if (values.length > 1) {
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
if (context.getSourceCode().getText(<any>values[i - 1].message) !== context.getSourceCode().getText(<any>values[i].message)) {
|
||||
context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
['Literal']: (node: any) => collectDoubleQuotedStrings(node),
|
||||
['ExpressionStatement[directive] Literal:exit']: (node: any) => doubleQuotedStringLiterals.delete(node),
|
||||
|
||||
// localize(...)
|
||||
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node),
|
||||
|
||||
// localize2(...)
|
||||
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize2"]:exit']: (node: any) => visitLocalizeCall(node),
|
||||
|
||||
// vscode.l10n.t(...)
|
||||
['CallExpression[callee.type="MemberExpression"][callee.object.property.name="l10n"][callee.property.name="t"]:exit']: (node: any) => visitL10NCall(node),
|
||||
|
||||
// l10n.t(...)
|
||||
['CallExpression[callee.object.name="l10n"][callee.property.name="t"]:exit']: (node: any) => visitL10NCall(node),
|
||||
|
||||
['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
|
||||
['CallExpression[callee.name="localize2"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
|
||||
['Program:exit']: reportBadStringsAndBadKeys,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// FORKED FROM https://github.com/eslint/eslint/blob/b23ad0d789a909baf8d7c41a35bc53df932eaf30/lib/rules/no-unused-expressions.js
|
||||
// and added support for `OptionalCallExpression`, see https://github.com/facebook/create-react-app/issues/8107 and https://github.com/eslint/eslint/issues/12642
|
||||
|
||||
/**
|
||||
* @fileoverview Flag expressions in statement position that do not side effect
|
||||
* @author Michael Ficarra
|
||||
*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import * as ESTree from 'estree';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
|
||||
docs: {
|
||||
description: 'disallow unused expressions',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: 'https://eslint.org/docs/rules/no-unused-expressions'
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowShortCircuit: {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
allowTernary: {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
allowTaggedTemplates: {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context: eslint.Rule.RuleContext) {
|
||||
const config = context.options[0] || {},
|
||||
allowShortCircuit = config.allowShortCircuit || false,
|
||||
allowTernary = config.allowTernary || false,
|
||||
allowTaggedTemplates = config.allowTaggedTemplates || false;
|
||||
|
||||
|
||||
/**
|
||||
* @param node any node
|
||||
* @returns whether the given node structurally represents a directive
|
||||
*/
|
||||
function looksLikeDirective(node: TSESTree.Node): boolean {
|
||||
return node.type === 'ExpressionStatement' &&
|
||||
node.expression.type === 'Literal' && typeof node.expression.value === 'string';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param predicate ([a] -> Boolean) the function used to make the determination
|
||||
* @param list the input list
|
||||
* @returns the leading sequence of members in the given list that pass the given predicate
|
||||
*/
|
||||
function takeWhile<T>(predicate: (item: T) => boolean, list: T[]): T[] {
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
if (!predicate(list[i])) {
|
||||
return list.slice(0, i);
|
||||
}
|
||||
}
|
||||
return list.slice();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param node a Program or BlockStatement node
|
||||
* @returns the leading sequence of directive nodes in the given node's body
|
||||
*/
|
||||
function directives(node: TSESTree.Program | TSESTree.BlockStatement): TSESTree.Node[] {
|
||||
return takeWhile(looksLikeDirective, node.body);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param node any node
|
||||
* @param ancestors the given node's ancestors
|
||||
* @returns whether the given node is considered a directive in its current position
|
||||
*/
|
||||
function isDirective(node: TSESTree.Node, ancestors: TSESTree.Node[]): boolean {
|
||||
const parent = ancestors[ancestors.length - 1],
|
||||
grandparent = ancestors[ancestors.length - 2];
|
||||
|
||||
return (parent.type === 'Program' || parent.type === 'BlockStatement' &&
|
||||
(/Function/u.test(grandparent.type))) &&
|
||||
directives(parent).indexOf(node) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags.
|
||||
* @param node any node
|
||||
* @returns whether the given node is a valid expression
|
||||
*/
|
||||
function isValidExpression(node: TSESTree.Node): boolean {
|
||||
if (allowTernary) {
|
||||
|
||||
// Recursive check for ternary and logical expressions
|
||||
if (node.type === 'ConditionalExpression') {
|
||||
return isValidExpression(node.consequent) && isValidExpression(node.alternate);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowShortCircuit) {
|
||||
if (node.type === 'LogicalExpression') {
|
||||
return isValidExpression(node.right);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.type === 'ExpressionStatement') {
|
||||
return isValidExpression(node.expression);
|
||||
}
|
||||
|
||||
return /^(?:Assignment|OptionalCall|Call|New|Update|Yield|Await|Chain)Expression$/u.test(node.type) ||
|
||||
(node.type === 'UnaryExpression' && ['delete', 'void'].indexOf(node.operator) >= 0);
|
||||
}
|
||||
|
||||
return {
|
||||
ExpressionStatement(node: TSESTree.ExpressionStatement) {
|
||||
if (!isValidExpression(node.expression) && !isDirective(node, <TSESTree.Node[]>context.sourceCode.getAncestors(node))) {
|
||||
context.report({ node: <ESTree.Node>node, message: `Expected an assignment or function call and instead saw an expression. ${node.expression}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
/**
|
||||
* Enforces that all parameter properties have an explicit access modifier (public, protected, private).
|
||||
*
|
||||
* This catches a common bug where a service is accidentally made public by simply writing: `readonly prop: Foo`
|
||||
*/
|
||||
export = new class implements eslint.Rule.RuleModule {
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
function check(inNode: any) {
|
||||
const node: TSESTree.TSParameterProperty = inNode;
|
||||
|
||||
// For now, only apply to injected services
|
||||
const firstDecorator = node.decorators?.at(0);
|
||||
if (
|
||||
firstDecorator?.expression.type !== 'Identifier'
|
||||
|| !firstDecorator.expression.name.endsWith('Service')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.accessibility) {
|
||||
context.report({
|
||||
node: inNode,
|
||||
message: 'Parameter properties must have an explicit access modifier.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
['TSParameterProperty']: check,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import { readFileSync } from 'fs';
|
||||
import { createImportRuleListener } from './utils';
|
||||
|
||||
|
||||
export = new class TranslationRemind implements eslint.Rule.RuleModule {
|
||||
|
||||
private static NLS_MODULE = 'vs/nls';
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
missing: 'Please add \'{{resource}}\' to ./build/lib/i18n.resources.json file to use translations here.'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return createImportRuleListener((node, path) => this._checkImport(context, node, path));
|
||||
}
|
||||
|
||||
private _checkImport(context: eslint.Rule.RuleContext, node: TSESTree.Node, path: string) {
|
||||
|
||||
if (path !== TranslationRemind.NLS_MODULE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFile = context.getFilename();
|
||||
const matchService = currentFile.match(/vs\/workbench\/services\/\w+/);
|
||||
const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/);
|
||||
if (!matchService && !matchPart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resource = matchService ? matchService[0] : matchPart![0];
|
||||
let resourceDefined = false;
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = readFileSync('./build/lib/i18n.resources.json', 'utf8');
|
||||
} catch (e) {
|
||||
console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.');
|
||||
return;
|
||||
}
|
||||
const workbenchResources = JSON.parse(json).workbench;
|
||||
|
||||
workbenchResources.forEach((existingResource: any) => {
|
||||
if (existingResource.name === resource) {
|
||||
resourceDefined = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (!resourceDefined) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: 'missing',
|
||||
data: { resource }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const glob = require('glob');
|
||||
const path = require('path');
|
||||
|
||||
require('ts-node').register({ experimentalResolver: true, transpileOnly: true });
|
||||
|
||||
// Re-export all .ts files as rules
|
||||
const rules = {};
|
||||
glob.sync(`${__dirname}/*.ts`).forEach((file) => {
|
||||
rules[path.basename(file, '.ts')] = require(file);
|
||||
});
|
||||
|
||||
exports.rules = rules;
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"alwaysStrict": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"newLine": "lf",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export function createImportRuleListener(validateImport: (node: TSESTree.Literal, value: string) => any): eslint.Rule.RuleListener {
|
||||
|
||||
function _checkImport(node: TSESTree.Node | null) {
|
||||
if (node && node.type === 'Literal' && typeof node.value === 'string') {
|
||||
validateImport(node, node.value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// import ??? from 'module'
|
||||
ImportDeclaration: (node: any) => {
|
||||
_checkImport((<TSESTree.ImportDeclaration>node).source);
|
||||
},
|
||||
// import('module').then(...) OR await import('module')
|
||||
['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node: any) => {
|
||||
_checkImport(node);
|
||||
},
|
||||
// import foo = ...
|
||||
['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node: any) => {
|
||||
_checkImport(node);
|
||||
},
|
||||
// export ?? from 'module'
|
||||
ExportAllDeclaration: (node: any) => {
|
||||
_checkImport((<TSESTree.ExportAllDeclaration>node).source);
|
||||
},
|
||||
// export {foo} from 'module'
|
||||
ExportNamedDeclaration: (node: any) => {
|
||||
_checkImport((<TSESTree.ExportNamedDeclaration>node).source);
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiProviderNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
noToken: 'Function lacks a cancellation token, preferable as last argument',
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
return {
|
||||
['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature[key.name=/^(provide|resolve).+/]']: (node: any) => {
|
||||
|
||||
let found = false;
|
||||
for (const param of (<TSESTree.TSMethodSignature>node).params) {
|
||||
if (param.type === AST_NODE_TYPES.Identifier) {
|
||||
found = found || param.name === 'token';
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noToken'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#creating-objects' },
|
||||
messages: { sync: '`createXYZ`-functions are constructor-replacements and therefore must return sync', },
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
return {
|
||||
['TSDeclareFunction Identifier[name=/create.*/]']: (node: any) => {
|
||||
|
||||
const decl = <TSESTree.FunctionDeclaration>(<TSESTree.Identifier>node).parent;
|
||||
|
||||
if (decl.returnType?.typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) {
|
||||
return;
|
||||
}
|
||||
if (decl.returnType.typeAnnotation.typeName.type !== AST_NODE_TYPES.Identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ident = decl.returnType.typeAnnotation.typeName.name;
|
||||
if (ident === 'Promise' || ident === 'Thenable') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'sync'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiEventNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
private static _nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/;
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
docs: {
|
||||
url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#event-naming'
|
||||
},
|
||||
messages: {
|
||||
naming: 'Event names must follow this patten: `on[Did|Will]<Verb><Subject>`',
|
||||
verb: 'Unknown verb \'{{verb}}\' - is this really a verb? Iff so, then add this verb to the configuration',
|
||||
subject: 'Unknown subject \'{{subject}}\' - This subject has not been used before but it should refer to something in the API',
|
||||
unknown: 'UNKNOWN event declaration, lint-rule needs tweaking'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const config = <{ allowed: string[]; verbs: string[] }>context.options[0];
|
||||
const allowed = new Set(config.allowed);
|
||||
const verbs = new Set(config.verbs);
|
||||
|
||||
return {
|
||||
['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: any) => {
|
||||
|
||||
const def = (<TSESTree.Identifier>node).parent?.parent?.parent;
|
||||
const ident = this.getIdent(def);
|
||||
|
||||
if (!ident) {
|
||||
// event on unknown structure...
|
||||
return context.report({
|
||||
node,
|
||||
message: 'unknown'
|
||||
});
|
||||
}
|
||||
|
||||
if (allowed.has(ident.name)) {
|
||||
// configured exception
|
||||
return;
|
||||
}
|
||||
|
||||
const match = ApiEventNaming._nameRegExp.exec(ident.name);
|
||||
if (!match) {
|
||||
context.report({
|
||||
node: ident,
|
||||
messageId: 'naming'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// check that <verb> is spelled out (configured) as verb
|
||||
if (!verbs.has(match[2].toLowerCase())) {
|
||||
context.report({
|
||||
node: ident,
|
||||
messageId: 'verb',
|
||||
data: { verb: match[2] }
|
||||
});
|
||||
}
|
||||
|
||||
// check that a subject (if present) has occurred
|
||||
if (match[3]) {
|
||||
const regex = new RegExp(match[3], 'ig');
|
||||
const parts = context.getSourceCode().getText().split(regex);
|
||||
if (parts.length < 3) {
|
||||
context.report({
|
||||
node: ident,
|
||||
messageId: 'subject',
|
||||
data: { subject: match[3] }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getIdent(def: TSESTree.Node | undefined): TSESTree.Identifier | undefined {
|
||||
if (!def) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (def.type === AST_NODE_TYPES.Identifier) {
|
||||
return def;
|
||||
} else if ((def.type === AST_NODE_TYPES.TSPropertySignature || def.type === AST_NODE_TYPES.PropertyDefinition) && def.key.type === AST_NODE_TYPES.Identifier) {
|
||||
return def.key;
|
||||
}
|
||||
|
||||
return this.getIdent(def.parent);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiInterfaceNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
private static _nameRegExp = /^I[A-Z]/;
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
naming: 'Interfaces must not be prefixed with uppercase `I`',
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
return {
|
||||
['TSInterfaceDeclaration Identifier']: (node: any) => {
|
||||
|
||||
const name = (<TSESTree.Identifier>node).name;
|
||||
if (ApiInterfaceNaming._nameRegExp.test(name)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'naming'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#enums' },
|
||||
messages: { useEnum: 'Use enums, not literal-or-types', },
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return {
|
||||
['TSTypeAnnotation TSUnionType']: (node: any) => {
|
||||
if ((<TSESTree.TSUnionType>node).types.every(value => value.type === 'TSLiteralType')) {
|
||||
context.report({
|
||||
node: node,
|
||||
messageId: 'useEnum'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiProviderNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
naming: 'A provider should only have functions like provideXYZ or resolveXYZ',
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
private static _providerFunctionNames = /^(provide|resolve|prepare).+/;
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const config = <{ allowed: string[] }>context.options[0];
|
||||
const allowed = new Set(config.allowed);
|
||||
|
||||
return {
|
||||
['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature']: (node: any) => {
|
||||
|
||||
|
||||
const interfaceName = (<TSESTree.TSInterfaceDeclaration>(<TSESTree.Identifier>node).parent?.parent).id.name;
|
||||
if (allowed.has(interfaceName)) {
|
||||
// allowed
|
||||
return;
|
||||
}
|
||||
|
||||
const methodName = (<any>(<TSESTree.TSMethodSignatureNonComputedName>node).key).name;
|
||||
|
||||
if (!ApiProviderNaming._providerFunctionNames.test(methodName)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'naming'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
export = new class ApiTypeDiscrimination implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines' },
|
||||
messages: {
|
||||
noTypeDiscrimination: 'Do not use type discrimination properties'
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return {
|
||||
['TSPropertySignature[optional=false] TSTypeAnnotation TSLiteralType Literal']: (node: any) => {
|
||||
const raw = String((<TSESTree.Literal>node).raw);
|
||||
|
||||
if (/^('|").*\1$/.test(raw)) {
|
||||
|
||||
context.report({
|
||||
node: node,
|
||||
messageId: 'noTypeDiscrimination'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class VscodeDtsUseExport implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
useExport: `Public api types must use 'export'`,
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
return {
|
||||
['TSModuleDeclaration :matches(TSInterfaceDeclaration, ClassDeclaration, VariableDeclaration, TSEnumDeclaration, TSTypeAliasDeclaration)']: (node: any) => {
|
||||
const parent = (<TSESTree.Node>node).parent;
|
||||
if (parent && parent.type !== TSESTree.AST_NODE_TYPES.ExportNamedDeclaration) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'useExport'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
|
||||
export = new class ApiEventNaming implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
usage: 'Use the Thenable-type instead of the Promise type',
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
|
||||
|
||||
return {
|
||||
['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node: any) => {
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'usage',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as eslint from 'eslint';
|
||||
import type * as estree from 'estree';
|
||||
|
||||
export = new class ApiVsCodeInComments implements eslint.Rule.RuleModule {
|
||||
|
||||
readonly meta: eslint.Rule.RuleMetaData = {
|
||||
messages: {
|
||||
comment: `Don't use the term 'vs code' in comments`
|
||||
},
|
||||
schema: false,
|
||||
};
|
||||
|
||||
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
return {
|
||||
['Program']: (_node: any) => {
|
||||
|
||||
for (const comment of sourceCode.getAllComments()) {
|
||||
if (comment.type !== 'Block') {
|
||||
continue;
|
||||
}
|
||||
if (!comment.range) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startIndex = comment.range[0] + '/*'.length;
|
||||
const re = /vs code/ig;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = re.exec(comment.value))) {
|
||||
// Allow using 'VS Code' in quotes
|
||||
if (comment.value[match.index - 1] === `'` && comment.value[match.index + match[0].length] === `'`) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Types for eslint seem incorrect
|
||||
const start = sourceCode.getLocFromIndex(startIndex + match.index) as any as estree.Position;
|
||||
const end = sourceCode.getLocFromIndex(startIndex + match.index + match[0].length) as any as estree.Position;
|
||||
context.report({
|
||||
messageId: 'comment',
|
||||
loc: { start, end }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
# https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt
|
||||
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
|
||||
|
||||
# mjbvz: Fix spacing
|
||||
13f4f052582bcec3d6c6c6a70d995c9dee2cac13
|
||||
|
||||
# mjbvz: Add script to run build with noImplicitOverride
|
||||
ae1452eea678f5266ef513f22dacebb90955d6c9
|
||||
|
||||
# alexdima: Revert "bump version"
|
||||
537ba0ef1791c090bb18bc68d727816c0451c117
|
||||
|
||||
# alexdima: bump version
|
||||
387a0dcb82df729e316ca2518a9ed81a75482b18
|
||||
|
||||
# joaomoreno: add ghooks dev dependency
|
||||
0dfc06e0f9de5925de792cdf9f0e6597bb25908f
|
||||
|
||||
# joaomoreno: line endings
|
||||
12ab70d329a13dd5b18d892cd40edd7138259bc3
|
||||
|
||||
# mjbvz: organize imports
|
||||
494cbbd02d67e87727ec885f98d19551aa33aad1
|
||||
a3cb14be7f2cceadb17adf843675b1a59537dbbd
|
||||
ee1655a82ebdfd38bf8792088a6602c69f7bbd94
|
||||
|
||||
# jrieken: new eslint-rule
|
||||
4a130c40ed876644ed8af2943809d08221375408
|
||||
|
||||
# bpasero: ESM migration
|
||||
6b924c51528e663dda5091a1493229a361676aca
|
||||
@@ -0,0 +1,11 @@
|
||||
* text=auto
|
||||
|
||||
LICENSE.txt eol=crlf
|
||||
ThirdPartyNotices.txt eol=crlf
|
||||
|
||||
*.bat eol=crlf
|
||||
*.cmd eol=crlf
|
||||
*.ps1 eol=lf
|
||||
*.sh eol=lf
|
||||
*.rtf -text
|
||||
**/*.json linguist-language=jsonc
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Issue
|
||||
about: Submit an Issue to Void
|
||||
title: For VSCode-related issues (eg builds), please start the title with `[App]`. Otherwise, start it with `[Bug]` or `[Feature]`.
|
||||
---
|
||||
|
||||
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
|
||||
|
||||
2. Describe the issue/feature here!
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
import os, sys, json, datetime, pathlib, textwrap, requests
|
||||
from openai import OpenAI
|
||||
|
||||
REPO = "voideditor/void"
|
||||
CACHE_FILE = pathlib.Path(".github/triage_cache.json")
|
||||
STAMP_FILE = pathlib.Path(".github/last_triage.txt")
|
||||
|
||||
THEMES_MD = textwrap.dedent("""\
|
||||
1. 🔗 LLM Integration & Provider Support
|
||||
2. 🖥 App Build & Platform Compatibility
|
||||
3. 🎯 Prompt, Token, and Cost Management
|
||||
4. 🧩 Editor UX & Interaction Design
|
||||
5. 🤖 Agent & Automation Features
|
||||
6. ⚙️ System Config & Environment Setup
|
||||
7. 🗃 Meta: Feature Comparison, Structure, and Naming
|
||||
""").strip()
|
||||
|
||||
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
||||
headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}
|
||||
|
||||
|
||||
# ───────── helpers ────────────────────────────────────────────────────────
|
||||
def utc_iso_now() -> str:
|
||||
return datetime.datetime.utcnow().replace(microsecond=0, tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
def read_stamp() -> str:
|
||||
return STAMP_FILE.read_text().strip() if STAMP_FILE.exists() else "1970-01-01T00:00:00Z"
|
||||
|
||||
def save_stamp():
|
||||
STAMP_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STAMP_FILE.write_text(utc_iso_now())
|
||||
|
||||
def load_cache() -> dict[int, str]:
|
||||
return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
|
||||
|
||||
def save_cache(d: dict[int, str]):
|
||||
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CACHE_FILE.write_text(json.dumps(d, indent=2))
|
||||
|
||||
def fetch_open_issues(since_iso: str | None = None) -> list[dict]:
|
||||
issues, page = [], 1
|
||||
while True:
|
||||
url = (
|
||||
f"https://api.github.com/repos/{REPO}/issues"
|
||||
f"?state=open&per_page=100&page={page}"
|
||||
+ (f"&since={since_iso}" if since_iso else "")
|
||||
)
|
||||
chunk = requests.get(url, headers=headers).json()
|
||||
if not chunk or (isinstance(chunk, dict) and chunk.get("message")):
|
||||
break
|
||||
issues.extend(i for i in chunk if "pull_request" not in i)
|
||||
page += 1
|
||||
return issues
|
||||
|
||||
|
||||
# ───────── main ───────────────────────────────────────────────────────────
|
||||
last_stamp = read_stamp()
|
||||
changed = fetch_open_issues(since_iso=last_stamp)
|
||||
|
||||
# Fallback if **nothing** changed AND we have *no* existing output
|
||||
if not changed:
|
||||
cache_exists = CACHE_FILE.exists()
|
||||
wiki_exists = pathlib.Path("wiki/Issue-Categories.md").exists()
|
||||
if not cache_exists or not wiki_exists:
|
||||
# first run or someone wiped the wiki → build from scratch
|
||||
print("⏩ First run or empty wiki — fetching ALL open issues.", file=sys.stderr)
|
||||
changed = fetch_open_issues() # full list
|
||||
else:
|
||||
print(f"✅ No issues updated since {last_stamp}. Nothing to classify.", file=sys.stderr)
|
||||
save_stamp()
|
||||
sys.exit(0)
|
||||
|
||||
# ---------------------------------------------------------------- prompt
|
||||
issue_lines = "\n".join(f"- {i['title']} ({i['html_url']})" for i in changed)
|
||||
prompt = textwrap.dedent(f"""\
|
||||
You are an AI assistant helping triage GitHub issues into exactly 7 predefined themes.
|
||||
|
||||
Each issue must go into exactly one of the themes below:
|
||||
|
||||
{THEMES_MD}
|
||||
|
||||
Format your output in Markdown like:
|
||||
## 🎯 Prompt, Token, and Cost Management
|
||||
- [#123](https://github.com/org/repo/issues/123) – Title here
|
||||
|
||||
Classify these issues:
|
||||
{issue_lines}
|
||||
""")
|
||||
|
||||
resp = client.chat.completions.create(
|
||||
model="gpt-4.1",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.2,
|
||||
)
|
||||
|
||||
md = resp.choices[0].message.content
|
||||
|
||||
# ---------------------------------------------------------------- parse GPT
|
||||
new_map: dict[int, str] = {}
|
||||
current = None
|
||||
for ln in md.splitlines():
|
||||
if ln.startswith("##"):
|
||||
current = ln.lstrip("# ").strip()
|
||||
elif ln.lstrip().startswith("- [#"):
|
||||
try:
|
||||
num = int(ln.split("[#")[1].split("]")[0])
|
||||
new_map[num] = current
|
||||
except Exception:
|
||||
pass # ignore malformed lines
|
||||
|
||||
cache = load_cache()
|
||||
cache.update(new_map)
|
||||
save_cache(cache)
|
||||
save_stamp()
|
||||
|
||||
# ---------------------------------------------------------------- rebuild wiki
|
||||
order = [
|
||||
"🔗 LLM Integration & Provider Support",
|
||||
"🖥 App Build & Platform Compatibility",
|
||||
"🎯 Prompt, Token, and Cost Management",
|
||||
"🧩 Editor UX & Interaction Design",
|
||||
"🤖 Agent & Automation Features",
|
||||
"⚙️ System Config & Environment Setup",
|
||||
"🗃 Meta: Feature Comparison, Structure, and Naming",
|
||||
]
|
||||
|
||||
sections: dict[str, list[int]] = {t: [] for t in order}
|
||||
|
||||
# ── fetch ALL current open issues once (PRs filtered out) ────────────────
|
||||
title_map: dict[int, tuple[str, str]] = {}
|
||||
open_now: set[int] = set()
|
||||
|
||||
page = 1
|
||||
while True:
|
||||
batch = fetch_open_issues(since_iso=None) if page == 1 else []
|
||||
if not batch:
|
||||
break
|
||||
for it in batch:
|
||||
num = it["number"]
|
||||
title_map[num] = (it["title"], it["html_url"])
|
||||
open_now.add(num)
|
||||
page += 1
|
||||
|
||||
# 🧹 drop any cached IDs that are no longer open issues (e.g., became a PR or were closed)
|
||||
for stale in set(cache) - open_now:
|
||||
del cache[stale]
|
||||
save_cache(cache) # persist cleaned cache
|
||||
|
||||
# build sections from cleaned cache
|
||||
for num, theme in cache.items():
|
||||
if theme in sections: # extra safety
|
||||
sections[theme].append(num)
|
||||
|
||||
# ---------------------------------------------------------------- print roadmap
|
||||
for theme in order:
|
||||
issues = sections[theme]
|
||||
if issues:
|
||||
print(f"## {theme}")
|
||||
for n in sorted(issues):
|
||||
title, url = title_map.get(n, ("(missing)", f"https://github.com/{REPO}/issues/{n}"))
|
||||
print(f"- [#{n}]({url}) – {title}")
|
||||
print()
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Issue Triage to Wiki
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # every 6 hrs (UTC)
|
||||
|
||||
jobs:
|
||||
roadmap:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 1️⃣ Check out code (so the script and cache files are present)
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1 # shallow clone
|
||||
|
||||
# 2️⃣ Set up Python
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
# 3️⃣ Install dependencies
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install openai requests
|
||||
|
||||
# 4️⃣ Clone your fork’s Wiki
|
||||
- name: Clone your fork's Wiki
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.WIKI_TOKEN }}@github.com/${{ github.repository }}.wiki.git wiki
|
||||
|
||||
# 5️⃣ (Optional) Show repo tree for debugging
|
||||
- name: Show repo tree (debug)
|
||||
run: |
|
||||
echo "PWD: $(pwd)"
|
||||
ls -al
|
||||
ls -al .github/scripts || true
|
||||
ls -al void/.github/scripts || true
|
||||
|
||||
# 6️⃣ Generate roadmap and push only if it changed
|
||||
- name: Generate roadmap directly into wiki
|
||||
run: |
|
||||
python .github/scripts/issue_triage.py > wiki/_new.md
|
||||
if ! cmp -s wiki/_new.md wiki/Issue-Categories.md ; then
|
||||
mv wiki/_new.md wiki/Issue-Categories.md
|
||||
cd wiki
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Issue-Categories.md
|
||||
git commit -m "Auto-update Issue-Categories.md from GPT triage"
|
||||
git push
|
||||
else
|
||||
echo "No content change – skipping wiki update"
|
||||
fi
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
.DS_Store
|
||||
.cache
|
||||
npm-debug.log
|
||||
Thumbs.db
|
||||
node_modules/
|
||||
.build/
|
||||
.vscode/extensions/**/out/
|
||||
extensions/**/dist/
|
||||
/out*/
|
||||
/extensions/**/out/
|
||||
build/node_modules
|
||||
coverage/
|
||||
test_data/
|
||||
test-results/
|
||||
test-results.xml
|
||||
vscode.lsif
|
||||
vscode.db
|
||||
/.profile-oss
|
||||
/cli/target
|
||||
/cli/openssl
|
||||
product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
|
||||
# Void added these:
|
||||
.tmp/
|
||||
.tmp2/
|
||||
.tool-versions
|
||||
src/vs/workbench/contrib/void/browser/react/out/**
|
||||
src/vs/workbench/contrib/void/browser/react/src2/**
|
||||
@@ -0,0 +1,48 @@
|
||||
# Created for Void
|
||||
# To learn more about how to use Nix to configure your environment
|
||||
# see: https://developers.google.com/idx/guides/customize-idx-env
|
||||
{pkgs}: {
|
||||
# Which nixpkgs channel to use.
|
||||
channel = "stable-23.11"; # or "unstable"
|
||||
# Use https://search.nixos.org/packages to find packages
|
||||
packages = [
|
||||
pkgs.nodejs_20
|
||||
pkgs.yarn
|
||||
pkgs.nodePackages.pnpm
|
||||
pkgs.bun
|
||||
pkgs.gh
|
||||
];
|
||||
# Sets environment variables in the workspace
|
||||
env = {};
|
||||
idx = {
|
||||
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
|
||||
extensions = [
|
||||
# "vscodevim.vim"
|
||||
];
|
||||
workspace = {
|
||||
# Runs when a workspace is first created with this `dev.nix` file
|
||||
onCreate = {
|
||||
npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing";
|
||||
# Open editors for the following files by default, if they exist:
|
||||
default.openFiles = [
|
||||
# Cover all the variations of language, src-dir, router (app/pages)
|
||||
"pages/index.tsx" "pages/index.jsx"
|
||||
"src/pages/index.tsx" "src/pages/index.jsx"
|
||||
"app/page.tsx" "app/page.jsx"
|
||||
"src/app/page.tsx" "src/app/page.jsx"
|
||||
];
|
||||
};
|
||||
# To run something each time the workspace is (re)started, use the `onStart` hook
|
||||
};
|
||||
# Enable previews and customize configuration
|
||||
previews = {
|
||||
enable = true;
|
||||
previews = {
|
||||
web = {
|
||||
command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"];
|
||||
manager = "web";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"project": "src/tsconfig.json",
|
||||
"source": "./package.json",
|
||||
"package": "package.json",
|
||||
"out": "vscode.lsif"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Daniel Imms <daimms@microsoft.com> Daniel Imms <tyriar@tyriar.com>
|
||||
Raymond Zhao <raymondzhao@microsoft.com>
|
||||
Tyler Leonhardt <tyleonha@microsoft.com> Tyler Leonhardt <me@tylerleonhardt.com>
|
||||
João Moreno <joao.moreno@microsoft.com> João Moreno <mail@joaomoreno.com>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"maxReviewers": 2,
|
||||
"requiredOrgs": ["Microsoft"],
|
||||
"skipAlreadyAssignedPR": true,
|
||||
"skipAlreadyMentionedPR": true,
|
||||
"skipCollaboratorPR": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
disturl="https://electronjs.org/headers"
|
||||
target="34.3.2"
|
||||
ms_build_id="11161073"
|
||||
runtime="electron"
|
||||
build_from_source="true"
|
||||
legacy-peer-deps="true"
|
||||
timeout=180000
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
This is a fork of the VSCode repo called Void.
|
||||
|
||||
Most code we care about lives in src/vs/workbench/contrib/void.
|
||||
|
||||
You may often need to explore the full repo to find relevant parts of code.
|
||||
Look for services and built-in functions that you might need to use to solve the problem.
|
||||
|
||||
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
|
||||
|
||||
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
|
||||
|
||||
Never modify files outside src/vs/workbench/contrib/void without consulting with the user first.
|
||||
|
||||
All types that map from a value A to B should be called bOfA. For example, if you create a hashmap that goes from toolId to toolName, it should be called toolNameOfToolId, etc.
|
||||
|
||||
Do not run anything to validate your changes; tell the user what to do instead.
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const { defineConfig } = require('@vscode/test-cli');
|
||||
|
||||
/**
|
||||
* A list of extension folders who have opted into tests, or configuration objects.
|
||||
* Edit me to add more!
|
||||
*
|
||||
* @type {Array<Partial<import("@vscode/test-cli").TestConfiguration> & { label: string }>}
|
||||
*/
|
||||
const extensions = [
|
||||
{
|
||||
label: 'markdown-language-features',
|
||||
workspaceFolder: `extensions/markdown-language-features/test-workspace`,
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'ipynb',
|
||||
workspaceFolder: path.join(os.tmpdir(), `ipynb-${Math.floor(Math.random() * 100000)}`),
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'notebook-renderers',
|
||||
workspaceFolder: path.join(os.tmpdir(), `nbout-${Math.floor(Math.random() * 100000)}`),
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'vscode-colorize-tests',
|
||||
workspaceFolder: `extensions/vscode-colorize-tests/test`,
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'terminal-suggest',
|
||||
workspaceFolder: path.join(os.tmpdir(), `terminal-suggest-${Math.floor(Math.random() * 100000)}`),
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'vscode-colorize-perf-tests',
|
||||
workspaceFolder: `extensions/vscode-colorize-perf-tests/test`,
|
||||
mocha: { timeout: 6000_000 }
|
||||
},
|
||||
{
|
||||
label: 'configuration-editing',
|
||||
workspaceFolder: path.join(os.tmpdir(), `confeditout-${Math.floor(Math.random() * 100000)}`),
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'github-authentication',
|
||||
workspaceFolder: path.join(os.tmpdir(), `msft-auth-${Math.floor(Math.random() * 100000)}`),
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'microsoft-authentication',
|
||||
mocha: { timeout: 60_000 }
|
||||
},
|
||||
{
|
||||
label: 'vscode-api-tests-folder',
|
||||
extensionDevelopmentPath: `extensions/vscode-api-tests`,
|
||||
workspaceFolder: `extensions/vscode-api-tests/testWorkspace`,
|
||||
mocha: { timeout: 60_000 },
|
||||
files: 'extensions/vscode-api-tests/out/singlefolder-tests/**/*.test.js',
|
||||
},
|
||||
{
|
||||
label: 'vscode-api-tests-workspace',
|
||||
extensionDevelopmentPath: `extensions/vscode-api-tests`,
|
||||
workspaceFolder: `extensions/vscode-api-tests/testworkspace.code-workspace`,
|
||||
mocha: { timeout: 60_000 },
|
||||
files: 'extensions/vscode-api-tests/out/workspace-tests/**/*.test.js',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const defaultLaunchArgs = process.env.API_TESTS_EXTRA_ARGS?.split(' ') || [
|
||||
'--disable-telemetry', '--skip-welcome', '--skip-release-notes', `--crash-reporter-directory=${__dirname}/.build/crashes`, `--logsPath=${__dirname}/.build/logs/integration-tests`, '--no-cached-data', '--disable-updates', '--use-inmemory-secretstorage', '--disable-extensions', '--disable-workspace-trust'
|
||||
];
|
||||
|
||||
const config = defineConfig(extensions.map(extension => {
|
||||
/** @type {import('@vscode/test-cli').TestConfiguration} */
|
||||
const config = {
|
||||
platform: 'desktop',
|
||||
files: `extensions/${extension.label}/out/**/*.test.js`,
|
||||
extensionDevelopmentPath: `extensions/${extension.label}`,
|
||||
...extension,
|
||||
};
|
||||
|
||||
config.mocha ??= {};
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
let suite = '';
|
||||
if (process.env.VSCODE_BROWSER) {
|
||||
suite = `${process.env.VSCODE_BROWSER} Browser Integration ${config.label} tests`;
|
||||
} else if (process.env.REMOTE_VSCODE) {
|
||||
suite = `Remote Integration ${config.label} tests`;
|
||||
} else {
|
||||
suite = `Integration ${config.label} tests`;
|
||||
}
|
||||
|
||||
config.mocha.reporter = 'mocha-multi-reporters';
|
||||
config.mocha.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.platform || config.platform === 'desktop') {
|
||||
config.launchArgs = defaultLaunchArgs;
|
||||
config.useInstallation = {
|
||||
fromPath: process.env.INTEGRATION_TEST_ELECTRON_PATH || `${__dirname}/scripts/code.${process.platform === 'win32' ? 'bat' : 'sh'}`,
|
||||
};
|
||||
config.env = {
|
||||
...config.env,
|
||||
VSCODE_SKIP_PRELAUNCH: '1',
|
||||
};
|
||||
} else {
|
||||
// web configs not supported, yet
|
||||
}
|
||||
|
||||
return config;
|
||||
}));
|
||||
|
||||
export default config;
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"prependLicenseText"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the dependency"
|
||||
},
|
||||
"fullLicenseText": {
|
||||
"type": "array",
|
||||
"description": "The complete license text of the dependency",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"prependLicenseText": {
|
||||
"type": "array",
|
||||
"description": "A piece of text to prepend to the auto-detected license text of the dependency",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"fullLicenseText"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the dependency"
|
||||
},
|
||||
"fullLicenseText": {
|
||||
"type": "array",
|
||||
"description": "The complete license text of the dependency",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"prependLicenseText": {
|
||||
"type": "array",
|
||||
"description": "A piece of text to prepend to the auto-detected license text of the dependency",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"fullLicenseTextUri"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the dependency"
|
||||
},
|
||||
"fullLicenseTextUri": {
|
||||
"type": "string",
|
||||
"description": "The URI to the license text of this repository",
|
||||
"format": "uri"
|
||||
},
|
||||
"prependLicenseText": {
|
||||
"type": "array",
|
||||
"description": "A piece of text to prepend to the auto-detected license text of the dependency",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"github.vscode-pull-request-github",
|
||||
"ms-vscode.vscode-github-issue-notebooks",
|
||||
"ms-vscode.extension-test-runner",
|
||||
"jrieken.vscode-pr-pinger"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--enable-proposed-api=ms-vscode.vscode-selfhost-import-aid"
|
||||
],
|
||||
"name": "Launch Extension",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"request": "launch",
|
||||
"type": "extensionHost"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "always"
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "vscode-selfhost-import-aid",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-selfhost-import-aid",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.88.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "vscode-selfhost-import-aid",
|
||||
"displayName": "VS Code Selfhost Import Aid",
|
||||
"description": "Util to improve dealing with imports",
|
||||
"engines": {
|
||||
"vscode": "^1.88.0"
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"publisher": "ms-vscode",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:typescript"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/vscode.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"compile": "gulp compile-extension:vscode-selfhost-import-aid",
|
||||
"watch": "gulp watch-extension:vscode-selfhost-import-aid"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as ts from 'typescript';
|
||||
import * as path from 'path';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
const fileIndex = new class {
|
||||
|
||||
private _currentRun?: Thenable<void>;
|
||||
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private readonly _index = new Map<string, vscode.Uri>();
|
||||
|
||||
constructor() {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher('**/*.ts', false, true, false);
|
||||
this._disposables.push(watcher.onDidChange(e => { this._index.set(e.toString(), e); }));
|
||||
this._disposables.push(watcher.onDidDelete(e => { this._index.delete(e.toString()); }));
|
||||
this._disposables.push(watcher);
|
||||
|
||||
this._refresh(false);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const disposable of this._disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this._disposables = [];
|
||||
this._index.clear();
|
||||
}
|
||||
|
||||
async all(token: vscode.CancellationToken) {
|
||||
|
||||
await Promise.race([this._currentRun, new Promise<void>(resolve => token.onCancellationRequested(resolve))]);
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array.from(this._index.values());
|
||||
}
|
||||
|
||||
private _refresh(clear: boolean) {
|
||||
// TODO@jrieken LATEST API! findFiles2New
|
||||
this._currentRun = vscode.workspace.findFiles('src/vs/**/*.ts', '{**/node_modules/**,**/extensions/**}').then(all => {
|
||||
if (clear) {
|
||||
this._index.clear();
|
||||
}
|
||||
for (const item of all) {
|
||||
this._index.set(item.toString(), item);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const selector: vscode.DocumentSelector = 'typescript';
|
||||
|
||||
function findNodeAtPosition(document: vscode.TextDocument, node: ts.Node, position: vscode.Position): ts.Node | undefined {
|
||||
if (node.getStart() <= document.offsetAt(position) && node.getEnd() >= document.offsetAt(position)) {
|
||||
return ts.forEachChild(node, child => findNodeAtPosition(document, child, position)) || node;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findImportAt(document: vscode.TextDocument, position: vscode.Position): ts.ImportDeclaration | undefined {
|
||||
const sourceFile = ts.createSourceFile(document.fileName, document.getText(), ts.ScriptTarget.Latest, true);
|
||||
const node = findNodeAtPosition(document, sourceFile, position);
|
||||
if (node && ts.isStringLiteral(node) && ts.isImportDeclaration(node.parent)) {
|
||||
return node.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const completionProvider = new class implements vscode.CompletionItemProvider {
|
||||
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionList | undefined> {
|
||||
|
||||
const index = document.getText().lastIndexOf(' from \'');
|
||||
if (index < 0 || document.positionAt(index).line < position.line) {
|
||||
// line after last import is before position
|
||||
// -> no completion, safe a parse call
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const node = findImportAt(document, position);
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const range = new vscode.Range(document.positionAt(node.moduleSpecifier.pos), document.positionAt(node.moduleSpecifier.end));
|
||||
const uris = await fileIndex.all(token);
|
||||
|
||||
if (!uris) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = new vscode.CompletionList();
|
||||
result.isIncomplete = true;
|
||||
|
||||
for (const item of uris) {
|
||||
|
||||
if (!item.path.endsWith('.ts')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relativePath = path.relative(path.dirname(document.uri.path), item.path);
|
||||
relativePath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
||||
|
||||
const label = path.basename(item.path, path.extname(item.path));
|
||||
const insertText = ` '${relativePath.replace(/\.ts$/, '.js')}'`;
|
||||
const filterText = ` '${label}'`;
|
||||
|
||||
const completion = new vscode.CompletionItem({
|
||||
label: label,
|
||||
description: vscode.workspace.asRelativePath(item),
|
||||
});
|
||||
completion.kind = vscode.CompletionItemKind.File;
|
||||
completion.insertText = insertText;
|
||||
completion.filterText = filterText;
|
||||
completion.range = range;
|
||||
|
||||
result.items.push(completion);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
class ImportCodeActions implements vscode.CodeActionProvider {
|
||||
|
||||
static FixKind = vscode.CodeActionKind.QuickFix.append('esmImport');
|
||||
|
||||
static SourceKind = vscode.CodeActionKind.SourceFixAll.append('esmImport');
|
||||
|
||||
async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise<vscode.CodeAction[] | undefined> {
|
||||
|
||||
if (context.only && ImportCodeActions.SourceKind.intersects(context.only)) {
|
||||
return this._provideFixAll(document, context, token);
|
||||
}
|
||||
|
||||
return this._provideFix(document, range, context, token);
|
||||
}
|
||||
|
||||
private async _provideFixAll(document: vscode.TextDocument, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise<vscode.CodeAction[] | undefined> {
|
||||
|
||||
const diagnostics = context.diagnostics
|
||||
.filter(d => d.code === 2307)
|
||||
.sort((a, b) => b.range.start.compareTo(a.range.start));
|
||||
|
||||
if (diagnostics.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uris = await fileIndex.all(token);
|
||||
if (!uris) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = new vscode.CodeAction(`Fix All ESM Imports`, ImportCodeActions.SourceKind);
|
||||
result.edit = new vscode.WorkspaceEdit();
|
||||
result.diagnostics = [];
|
||||
|
||||
for (const diag of diagnostics) {
|
||||
|
||||
const actions = this._provideFixesForDiag(document, diag, uris);
|
||||
|
||||
if (actions.length === 0) {
|
||||
console.log(`ESM: no fixes for "${diag.message}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actions.length > 1) {
|
||||
console.log(`ESM: more than one fix for "${diag.message}", taking first`);
|
||||
console.log(actions);
|
||||
}
|
||||
|
||||
const [first] = actions;
|
||||
result.diagnostics.push(diag);
|
||||
|
||||
for (const [uri, edits] of first.edit!.entries()) {
|
||||
result.edit.set(uri, edits);
|
||||
}
|
||||
}
|
||||
// console.log(result.edit.get(document.uri));
|
||||
return [result];
|
||||
}
|
||||
|
||||
private async _provideFix(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise<vscode.CodeAction[] | undefined> {
|
||||
const uris = await fileIndex.all(token);
|
||||
if (!uris) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diag = context.diagnostics.find(d => d.code === 2307 && d.range.intersection(range));
|
||||
return diag && this._provideFixesForDiag(document, diag, uris);
|
||||
}
|
||||
|
||||
private _provideFixesForDiag(document: vscode.TextDocument, diag: vscode.Diagnostic, uris: Iterable<vscode.Uri>): vscode.CodeAction[] {
|
||||
|
||||
const node = findImportAt(document, diag.range.start)?.moduleSpecifier;
|
||||
if (!node || !ts.isStringLiteral(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodeRange = new vscode.Range(document.positionAt(node.pos), document.positionAt(node.end));
|
||||
const name = path.basename(node.text, path.extname(node.text));
|
||||
|
||||
const result: vscode.CodeAction[] = [];
|
||||
|
||||
for (const item of uris) {
|
||||
if (path.basename(item.path, path.extname(item.path)) === name) {
|
||||
let relativePath = path.relative(path.dirname(document.uri.path), item.path).replace(/\.ts$/, '.js');
|
||||
relativePath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
||||
|
||||
const action = new vscode.CodeAction(`Fix to '${relativePath}'`, ImportCodeActions.FixKind);
|
||||
action.edit = new vscode.WorkspaceEdit();
|
||||
action.edit.replace(document.uri, nodeRange, ` '${relativePath}'`);
|
||||
action.diagnostics = [diag];
|
||||
result.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
context.subscriptions.push(fileIndex);
|
||||
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(selector, completionProvider));
|
||||
context.subscriptions.push(vscode.languages.registerCodeActionsProvider(selector, new ImportCodeActions(), { providedCodeActionKinds: [ImportCodeActions.FixKind, ImportCodeActions.SourceKind] }));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../../extensions/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
"types": [
|
||||
"node",
|
||||
"mocha",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../../src/vscode-dts/vscode.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--enable-proposed-api=ms-vscode.vscode-selfhost-test-provider"],
|
||||
"name": "Launch Extension",
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||
"request": "launch",
|
||||
"type": "extensionHost"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "always"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "vscode-selfhost-test-provider",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vscode-selfhost-test-provider",
|
||||
"version": "0.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"ansi-styles": "^5.2.0",
|
||||
"cockatiel": "^3.1.3",
|
||||
"istanbul-to-vscode": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "20.x"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.88.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="
|
||||
},
|
||||
"node_modules/@types/mocha": {
|
||||
"version": "10.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz",
|
||||
"integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
|
||||
"integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cockatiel": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.3.tgz",
|
||||
"integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-to-vscode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz",
|
||||
"integrity": "sha512-V9Hhr7kX3UvkvkaT1lK3AmCRPkaIAIogQBrduTpNiLTkp1eVsybnJhWiDSVeCQap/3aGeZ2019oIivhX9MNsCQ==",
|
||||
"dependencies": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "vscode-selfhost-test-provider",
|
||||
"displayName": "VS Code Selfhost Test Provider",
|
||||
"description": "Test provider for the VS Code project",
|
||||
"enabledApiProposals": [
|
||||
"testObserver",
|
||||
"testRelatedCode"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.88.0"
|
||||
},
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "selfhost-test-provider.updateSnapshot",
|
||||
"title": "Update Snapshot",
|
||||
"category": "Testing",
|
||||
"icon": "$(merge)"
|
||||
},
|
||||
{
|
||||
"command": "selfhost-test-provider.openFailureLog",
|
||||
"title": "Open Selfhost Failure Logs",
|
||||
"category": "Testing",
|
||||
"icon": "$(merge)"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "selfhost-test-provider.updateSnapshot",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"testing/message/context": [
|
||||
{
|
||||
"command": "selfhost-test-provider.updateSnapshot",
|
||||
"group": "inline@1",
|
||||
"when": "testMessage == isSelfhostSnapshotMessage && !testResultOutdated"
|
||||
}
|
||||
],
|
||||
"testing/message/content": [
|
||||
{
|
||||
"command": "selfhost-test-provider.updateSnapshot",
|
||||
"when": "testMessage == isSelfhostSnapshotMessage && !testResultOutdated"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"icon": "icon.png",
|
||||
"version": "0.4.0",
|
||||
"publisher": "ms-vscode",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:src/vs/loader.js"
|
||||
],
|
||||
"workspaceTrust": {
|
||||
"request": "onDemand",
|
||||
"description": "Trust is required to execute tests in the workspace."
|
||||
},
|
||||
"main": "./out/extension.js",
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "avoid"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/vscode.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"compile": "gulp compile-extension:vscode-selfhost-test-provider",
|
||||
"watch": "gulp watch-extension:vscode-selfhost-test-provider",
|
||||
"test": "npx mocha --ui tdd 'out/*.test.js'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "20.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"ansi-styles": "^5.2.0",
|
||||
"cockatiel": "^3.1.3",
|
||||
"istanbul-to-vscode": "^2.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IstanbulCoverageContext } from 'istanbul-to-vscode';
|
||||
import * as vscode from 'vscode';
|
||||
import { SearchStrategy, SourceLocationMapper, SourceMapStore } from './testOutputScanner';
|
||||
import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling';
|
||||
|
||||
export const istanbulCoverageContext = new IstanbulCoverageContext();
|
||||
|
||||
/**
|
||||
* Tracks coverage in per-script coverage mode. There are two modes of coverage
|
||||
* in this extension: generic istanbul reports, and reports from the runtime
|
||||
* sent before and after each test case executes. This handles the latter.
|
||||
*/
|
||||
export class PerTestCoverageTracker {
|
||||
private readonly scripts = new Map</* script ID */ string, Script>();
|
||||
|
||||
constructor(private readonly maps: SourceMapStore) { }
|
||||
|
||||
public add(coverage: IScriptCoverage, test?: vscode.TestItem) {
|
||||
const script = this.scripts.get(coverage.scriptId);
|
||||
if (script) {
|
||||
return script.add(coverage, test);
|
||||
}
|
||||
// ignore internals and node_modules
|
||||
if (!coverage.url.startsWith('file://') || coverage.url.includes('node_modules')) {
|
||||
return;
|
||||
}
|
||||
if (!coverage.source) {
|
||||
throw new Error('expected to have source the first time a script is seen');
|
||||
}
|
||||
|
||||
const src = new Script(vscode.Uri.parse(coverage.url), coverage.source, this.maps);
|
||||
this.scripts.set(coverage.scriptId, src);
|
||||
src.add(coverage, test);
|
||||
}
|
||||
|
||||
public async report(run: vscode.TestRun) {
|
||||
await Promise.all(Array.from(this.scripts.values()).map(s => s.report(run)));
|
||||
}
|
||||
}
|
||||
|
||||
class Script {
|
||||
private converter: OffsetToPosition;
|
||||
|
||||
/** Tracking the overall coverage for the file */
|
||||
private overall = new ScriptCoverageTracker();
|
||||
/** Range tracking per-test item */
|
||||
private readonly perItem = new Map<vscode.TestItem, ScriptCoverageTracker>();
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
source: string,
|
||||
private readonly maps: SourceMapStore
|
||||
) {
|
||||
this.converter = new OffsetToPosition(source);
|
||||
}
|
||||
|
||||
public add(coverage: IScriptCoverage, test?: vscode.TestItem) {
|
||||
this.overall.add(coverage);
|
||||
if (test) {
|
||||
const p = new ScriptCoverageTracker();
|
||||
p.add(coverage);
|
||||
this.perItem.set(test, p);
|
||||
}
|
||||
}
|
||||
|
||||
public async report(run: vscode.TestRun) {
|
||||
const mapper = await this.maps.getSourceLocationMapper(this.uri.toString());
|
||||
const originalUri = (await this.maps.getSourceFile(this.uri.toString())) || this.uri;
|
||||
run.addCoverage(this.overall.report(originalUri, this.converter, mapper, this.perItem));
|
||||
}
|
||||
}
|
||||
|
||||
class ScriptCoverageTracker {
|
||||
private coverage = new RangeCoverageTracker();
|
||||
|
||||
public add(coverage: IScriptCoverage) {
|
||||
for (const range of RangeCoverageTracker.initializeBlocks(coverage.functions)) {
|
||||
this.coverage.setCovered(range.start, range.end, range.covered);
|
||||
}
|
||||
}
|
||||
|
||||
public *toDetails(
|
||||
uri: vscode.Uri,
|
||||
convert: OffsetToPosition,
|
||||
mapper: SourceLocationMapper | undefined,
|
||||
) {
|
||||
for (const range of this.coverage) {
|
||||
if (range.start === range.end) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startCov = convert.toLineColumn(range.start);
|
||||
let start = new vscode.Position(startCov.line, startCov.column);
|
||||
|
||||
const endCov = convert.toLineColumn(range.end);
|
||||
let end = new vscode.Position(endCov.line, endCov.column);
|
||||
if (mapper) {
|
||||
const startMap = mapper(start.line, start.character, SearchStrategy.FirstAfter);
|
||||
const endMap = startMap && mapper(end.line, end.character, SearchStrategy.FirstBefore);
|
||||
if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
start = startMap.range.start;
|
||||
end = endMap.range.end;
|
||||
}
|
||||
|
||||
for (let i = start.line; i <= end.line; i++) {
|
||||
yield new vscode.StatementCoverage(
|
||||
range.covered,
|
||||
new vscode.Range(
|
||||
new vscode.Position(i, i === start.line ? start.character : 0),
|
||||
new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the script's coverage for the test run.
|
||||
*
|
||||
* If a source location mapper is given, it assumes the `uri` is the mapped
|
||||
* URI, and that any unmapped locations/outside the URI should be ignored.
|
||||
*/
|
||||
public report(
|
||||
uri: vscode.Uri,
|
||||
convert: OffsetToPosition,
|
||||
mapper: SourceLocationMapper | undefined,
|
||||
items: Map<vscode.TestItem, ScriptCoverageTracker>,
|
||||
): V8CoverageFile {
|
||||
const file = new V8CoverageFile(uri, items, convert, mapper);
|
||||
for (const detail of this.toDetails(uri, convert, mapper)) {
|
||||
file.add(detail);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
export class V8CoverageFile extends vscode.FileCoverage {
|
||||
public details: vscode.StatementCoverage[] = [];
|
||||
|
||||
constructor(
|
||||
uri: vscode.Uri,
|
||||
private readonly perTest: Map<vscode.TestItem, ScriptCoverageTracker>,
|
||||
private readonly convert: OffsetToPosition,
|
||||
private readonly mapper: SourceLocationMapper | undefined,
|
||||
) {
|
||||
super(uri, { covered: 0, total: 0 }, undefined, undefined, [...perTest.keys()]);
|
||||
}
|
||||
|
||||
public add(detail: vscode.StatementCoverage) {
|
||||
this.details.push(detail);
|
||||
this.statementCoverage.total++;
|
||||
if (detail.executed) {
|
||||
this.statementCoverage.covered++;
|
||||
}
|
||||
}
|
||||
|
||||
public testDetails(test: vscode.TestItem): vscode.FileCoverageDetail[] {
|
||||
const t = this.perTest.get(test);
|
||||
return t ? [...t.toDetails(this.uri, this.convert, this.mapper)] : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Debounces the function call for an interval.
|
||||
*/
|
||||
export function debounce(duration: number, fn: () => void): (() => void) & { clear: () => void } {
|
||||
let timeout: NodeJS.Timeout | void;
|
||||
const debounced = () => {
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
timeout = undefined;
|
||||
fn();
|
||||
}, duration);
|
||||
};
|
||||
|
||||
debounced.clear = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import { tmpdir } from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { V8CoverageFile } from './coverageProvider';
|
||||
import { FailingDeepStrictEqualAssertFixer } from './failingDeepStrictEqualAssertFixer';
|
||||
import { FailureTracker } from './failureTracker';
|
||||
import { registerSnapshotUpdate } from './snapshot';
|
||||
import { scanTestOutput } from './testOutputScanner';
|
||||
import {
|
||||
TestCase,
|
||||
TestFile,
|
||||
clearFileDiagnostics,
|
||||
guessWorkspaceFolder,
|
||||
itemData,
|
||||
} from './testTree';
|
||||
import { BrowserTestRunner, PlatformTestRunner, VSCodeTestRunner } from './vscodeTestRunner';
|
||||
import { ImportGraph } from './importGraph';
|
||||
|
||||
const TEST_FILE_PATTERN = 'src/vs/**/*.{test,integrationTest}.ts';
|
||||
|
||||
const getWorkspaceFolderForTestFile = (uri: vscode.Uri) =>
|
||||
(uri.path.endsWith('.test.ts') || uri.path.endsWith('.integrationTest.ts')) &&
|
||||
uri.path.includes('/src/vs/')
|
||||
? vscode.workspace.getWorkspaceFolder(uri)
|
||||
: undefined;
|
||||
|
||||
const browserArgs: [name: string, arg: string][] = [
|
||||
['Chrome', 'chromium'],
|
||||
['Firefox', 'firefox'],
|
||||
['Webkit', 'webkit'],
|
||||
];
|
||||
|
||||
type FileChangeEvent = { uri: vscode.Uri; removed: boolean };
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests');
|
||||
const fileChangedEmitter = new vscode.EventEmitter<FileChangeEvent>();
|
||||
|
||||
context.subscriptions.push(vscode.tests.registerTestFollowupProvider({
|
||||
async provideFollowup(_result, test, taskIndex, messageIndex, _token) {
|
||||
return [{
|
||||
title: '$(sparkle) Fix with Copilot',
|
||||
command: 'github.copilot.tests.fixTestFailure',
|
||||
arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }]
|
||||
}];
|
||||
},
|
||||
}));
|
||||
|
||||
let initialWatchPromise: Promise<vscode.Disposable> | undefined;
|
||||
const resolveHandler = async (test?: vscode.TestItem) => {
|
||||
if (!test) {
|
||||
if (!initialWatchPromise) {
|
||||
initialWatchPromise = startWatchingWorkspace(ctrl, fileChangedEmitter);
|
||||
context.subscriptions.push(await initialWatchPromise);
|
||||
} else {
|
||||
await initialWatchPromise;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = itemData.get(test);
|
||||
if (data instanceof TestFile) {
|
||||
// No need to watch this, updates will be triggered on file changes
|
||||
// either by the text document or file watcher.
|
||||
await data.updateFromDisk(ctrl, test);
|
||||
}
|
||||
};
|
||||
|
||||
ctrl.resolveHandler = resolveHandler;
|
||||
|
||||
guessWorkspaceFolder().then(folder => {
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const graph = new ImportGraph(
|
||||
folder.uri, async () => {
|
||||
await resolveHandler();
|
||||
return [...ctrl.items].map(([, item]) => item);
|
||||
}, uri => ctrl.items.get(uri.toString().toLowerCase()));
|
||||
ctrl.relatedCodeProvider = graph;
|
||||
|
||||
if (context.storageUri) {
|
||||
context.subscriptions.push(new FailureTracker(context.storageUri.fsPath, folder.uri.fsPath));
|
||||
}
|
||||
|
||||
context.subscriptions.push(fileChangedEmitter.event(e => graph.didChange(e.uri, e.removed)));
|
||||
});
|
||||
|
||||
const createRunHandler = (
|
||||
runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner },
|
||||
kind: vscode.TestRunProfileKind,
|
||||
args: string[] = []
|
||||
) => {
|
||||
const doTestRun = async (
|
||||
req: vscode.TestRunRequest,
|
||||
cancellationToken: vscode.CancellationToken
|
||||
) => {
|
||||
const folder = await guessWorkspaceFolder();
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runner = new runnerCtor(folder);
|
||||
const map = await getPendingTestMap(ctrl, req.include ?? gatherTestItems(ctrl.items));
|
||||
const task = ctrl.createTestRun(req);
|
||||
for (const test of map.values()) {
|
||||
task.enqueued(test);
|
||||
}
|
||||
|
||||
let coverageDir: string | undefined;
|
||||
let currentArgs = args;
|
||||
if (kind === vscode.TestRunProfileKind.Coverage) {
|
||||
// todo: browser runs currently don't support per-test coverage
|
||||
if (args.includes('--browser')) {
|
||||
coverageDir = path.join(
|
||||
tmpdir(),
|
||||
`vscode-test-coverage-${randomBytes(8).toString('hex')}`
|
||||
);
|
||||
currentArgs = [
|
||||
...currentArgs,
|
||||
'--coverage',
|
||||
'--coveragePath',
|
||||
coverageDir,
|
||||
'--coverageFormats',
|
||||
'json',
|
||||
];
|
||||
} else {
|
||||
currentArgs = [...currentArgs, '--per-test-coverage'];
|
||||
}
|
||||
}
|
||||
|
||||
return await scanTestOutput(
|
||||
map,
|
||||
task,
|
||||
kind === vscode.TestRunProfileKind.Debug
|
||||
? await runner.debug(task, currentArgs, req.include)
|
||||
: await runner.run(currentArgs, req.include),
|
||||
coverageDir,
|
||||
cancellationToken
|
||||
);
|
||||
};
|
||||
|
||||
return async (req: vscode.TestRunRequest, cancellationToken: vscode.CancellationToken) => {
|
||||
if (!req.continuous) {
|
||||
return doTestRun(req, cancellationToken);
|
||||
}
|
||||
|
||||
const queuedFiles = new Set<string>();
|
||||
let debounced: NodeJS.Timeout | undefined;
|
||||
|
||||
const listener = fileChangedEmitter.event(({ uri, removed }) => {
|
||||
clearTimeout(debounced);
|
||||
|
||||
if (req.include && !req.include.some(i => i.uri?.toString() === uri.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
queuedFiles.delete(uri.toString());
|
||||
} else {
|
||||
queuedFiles.add(uri.toString());
|
||||
}
|
||||
|
||||
debounced = setTimeout(() => {
|
||||
const include =
|
||||
req.include?.filter(t => t.uri && queuedFiles.has(t.uri?.toString())) ??
|
||||
[...queuedFiles]
|
||||
.map(f => getOrCreateFile(ctrl, vscode.Uri.parse(f)))
|
||||
.filter((f): f is vscode.TestItem => !!f);
|
||||
queuedFiles.clear();
|
||||
|
||||
doTestRun(
|
||||
new vscode.TestRunRequest(include, req.exclude, req.profile, true),
|
||||
cancellationToken
|
||||
);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
cancellationToken.onCancellationRequested(() => {
|
||||
clearTimeout(debounced);
|
||||
listener.dispose();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
ctrl.createRunProfile(
|
||||
'Run in Electron',
|
||||
vscode.TestRunProfileKind.Run,
|
||||
createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Run),
|
||||
true,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
ctrl.createRunProfile(
|
||||
'Debug in Electron',
|
||||
vscode.TestRunProfileKind.Debug,
|
||||
createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Debug),
|
||||
true,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
const coverage = ctrl.createRunProfile(
|
||||
'Coverage in Electron',
|
||||
vscode.TestRunProfileKind.Coverage,
|
||||
createRunHandler(PlatformTestRunner, vscode.TestRunProfileKind.Coverage),
|
||||
true,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
coverage.loadDetailedCoverage = async (_run, coverage) => coverage instanceof V8CoverageFile ? coverage.details : [];
|
||||
coverage.loadDetailedCoverageForTest = async (_run, coverage, test) => coverage instanceof V8CoverageFile ? coverage.testDetails(test) : [];
|
||||
|
||||
for (const [name, arg] of browserArgs) {
|
||||
const cfg = ctrl.createRunProfile(
|
||||
`Run in ${name}`,
|
||||
vscode.TestRunProfileKind.Run,
|
||||
createRunHandler(BrowserTestRunner, vscode.TestRunProfileKind.Run, [' --browser', arg]),
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
cfg.configureHandler = () => vscode.window.showInformationMessage(`Configuring ${name}`);
|
||||
|
||||
ctrl.createRunProfile(
|
||||
`Debug in ${name}`,
|
||||
vscode.TestRunProfileKind.Debug,
|
||||
createRunHandler(BrowserTestRunner, vscode.TestRunProfileKind.Debug, [
|
||||
'--browser',
|
||||
arg,
|
||||
'--debug-browser',
|
||||
]),
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function updateNodeForDocument(e: vscode.TextDocument) {
|
||||
const node = getOrCreateFile(ctrl, e.uri);
|
||||
const data = node && itemData.get(node);
|
||||
if (data instanceof TestFile) {
|
||||
data.updateFromContents(ctrl, e.getText(), node!);
|
||||
}
|
||||
}
|
||||
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
updateNodeForDocument(document);
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
ctrl,
|
||||
fileChangedEmitter.event(({ uri, removed }) => {
|
||||
if (!removed) {
|
||||
const node = getOrCreateFile(ctrl, uri);
|
||||
if (node) {
|
||||
ctrl.invalidateTestResults();
|
||||
}
|
||||
}
|
||||
}),
|
||||
vscode.workspace.onDidOpenTextDocument(updateNodeForDocument),
|
||||
vscode.workspace.onDidChangeTextDocument(e => updateNodeForDocument(e.document)),
|
||||
registerSnapshotUpdate(ctrl),
|
||||
new FailingDeepStrictEqualAssertFixer()
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
function getOrCreateFile(
|
||||
controller: vscode.TestController,
|
||||
uri: vscode.Uri
|
||||
): vscode.TestItem | undefined {
|
||||
const folder = getWorkspaceFolderForTestFile(uri);
|
||||
if (!folder) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = new TestFile(uri, folder);
|
||||
const existing = controller.items.get(data.getId());
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const file = controller.createTestItem(data.getId(), data.getLabel(), uri);
|
||||
controller.items.add(file);
|
||||
file.canResolveChildren = true;
|
||||
itemData.set(file, data);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
function gatherTestItems(collection: vscode.TestItemCollection) {
|
||||
const items: vscode.TestItem[] = [];
|
||||
collection.forEach(item => items.push(item));
|
||||
return items;
|
||||
}
|
||||
|
||||
async function startWatchingWorkspace(
|
||||
controller: vscode.TestController,
|
||||
fileChangedEmitter: vscode.EventEmitter<FileChangeEvent>
|
||||
) {
|
||||
const workspaceFolder = await guessWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
return new vscode.Disposable(() => undefined);
|
||||
}
|
||||
|
||||
const pattern = new vscode.RelativePattern(workspaceFolder, TEST_FILE_PATTERN);
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
|
||||
|
||||
watcher.onDidCreate(uri => {
|
||||
getOrCreateFile(controller, uri);
|
||||
fileChangedEmitter.fire({ removed: false, uri });
|
||||
});
|
||||
watcher.onDidChange(uri => fileChangedEmitter.fire({ removed: false, uri }));
|
||||
watcher.onDidDelete(uri => {
|
||||
fileChangedEmitter.fire({ removed: true, uri });
|
||||
clearFileDiagnostics(uri);
|
||||
controller.items.delete(uri.toString());
|
||||
});
|
||||
|
||||
for (const file of await vscode.workspace.findFiles(pattern)) {
|
||||
getOrCreateFile(controller, file);
|
||||
}
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
async function getPendingTestMap(ctrl: vscode.TestController, tests: Iterable<vscode.TestItem>) {
|
||||
const queue = [tests];
|
||||
const titleMap = new Map<string, vscode.TestItem>();
|
||||
while (queue.length) {
|
||||
for (const item of queue.pop()!) {
|
||||
const data = itemData.get(item);
|
||||
if (data instanceof TestFile) {
|
||||
if (!data.hasBeenRead) {
|
||||
await data.updateFromDisk(ctrl, item);
|
||||
}
|
||||
queue.push(gatherTestItems(item.children));
|
||||
} else if (data instanceof TestCase) {
|
||||
titleMap.set(data.fullName, item);
|
||||
} else {
|
||||
queue.push(gatherTestItems(item.children));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return titleMap;
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {
|
||||
commands,
|
||||
Disposable,
|
||||
languages,
|
||||
Position,
|
||||
Range,
|
||||
TestMessage,
|
||||
TestResultSnapshot,
|
||||
TestRunResult,
|
||||
tests,
|
||||
TextDocument,
|
||||
Uri,
|
||||
workspace,
|
||||
WorkspaceEdit,
|
||||
} from 'vscode';
|
||||
import { memoizeLast } from './memoize';
|
||||
import { getTestMessageMetadata } from './metadata';
|
||||
|
||||
const enum Constants {
|
||||
FixCommandId = 'selfhost-test.fix-test',
|
||||
}
|
||||
|
||||
export class FailingDeepStrictEqualAssertFixer {
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor() {
|
||||
this.disposables.push(
|
||||
commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => {
|
||||
const document = await workspace.openTextDocument(uri);
|
||||
|
||||
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position);
|
||||
if (!failingAssertion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedValueNode = failingAssertion.assertion.expectedValue;
|
||||
if (!expectedValueNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = document.positionAt(expectedValueNode.getStart());
|
||||
const end = document.positionAt(expectedValueNode.getEnd());
|
||||
|
||||
const edit = new WorkspaceEdit();
|
||||
edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue));
|
||||
await workspace.applyEdit(edit);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
languages.registerCodeActionsProvider('typescript', {
|
||||
provideCodeActions: (document, range) => {
|
||||
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start);
|
||||
if (!failingAssertion) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Fix Expected Value',
|
||||
command: Constants.FixCommandId,
|
||||
arguments: [document.uri, range.start],
|
||||
},
|
||||
];
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i;
|
||||
|
||||
const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
||||
|
||||
const formatJsonValue = (value: unknown) => {
|
||||
if (typeof value !== 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true);
|
||||
const outerExpression = src.statements[0] as ts.ExpressionStatement;
|
||||
const parenExpression = outerExpression.expression as ts.ParenthesizedExpression;
|
||||
|
||||
const unquoted = ts.transform(parenExpression, [
|
||||
context => (node: ts.Node) => {
|
||||
const visitor = (node: ts.Node): ts.Node =>
|
||||
ts.isPropertyAssignment(node) &&
|
||||
ts.isStringLiteralLike(node.name) &&
|
||||
identifierLikeRe.test(node.name.text)
|
||||
? ts.factory.createPropertyAssignment(
|
||||
ts.factory.createIdentifier(node.name.text),
|
||||
ts.visitNode(node.initializer, visitor) as ts.Expression
|
||||
)
|
||||
: ts.isStringLiteralLike(node) && node.text === '[undefined]'
|
||||
? ts.factory.createIdentifier('undefined')
|
||||
: ts.visitEachChild(node, visitor, context);
|
||||
|
||||
return ts.visitNode(node, visitor);
|
||||
},
|
||||
]);
|
||||
|
||||
return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src);
|
||||
};
|
||||
|
||||
/** Parses the source file, memoizing the last document so cursor moves are efficient */
|
||||
const parseSourceFile = memoizeLast((text: string) =>
|
||||
ts.createSourceFile('', text, ts.ScriptTarget.ES5, true)
|
||||
);
|
||||
|
||||
const assertionFailureMessageRe = /^Expected values to be strictly (deep-)?equal:/;
|
||||
|
||||
/** Gets information about the failing assertion at the poisition, if any. */
|
||||
function detectFailingDeepStrictEqualAssertion(
|
||||
document: TextDocument,
|
||||
position: Position
|
||||
): { assertion: StrictEqualAssertion; actualJSONValue: unknown } | undefined {
|
||||
const sf = parseSourceFile(document.getText());
|
||||
const offset = document.offsetAt(position);
|
||||
const assertion = StrictEqualAssertion.atPosition(sf, offset);
|
||||
if (!assertion) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startLine = document.positionAt(assertion.offsetStart).line;
|
||||
const messages = getAllTestStatusMessagesAt(document.uri, startLine);
|
||||
const strictDeepEqualMessage = messages.find(m =>
|
||||
assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value)
|
||||
);
|
||||
|
||||
if (!strictDeepEqualMessage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const metadata = getTestMessageMetadata(strictDeepEqualMessage);
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
assertion: assertion,
|
||||
actualJSONValue: metadata.actualValue,
|
||||
};
|
||||
}
|
||||
|
||||
class StrictEqualAssertion {
|
||||
/**
|
||||
* Extracts the assertion at the current node, if it is one.
|
||||
*/
|
||||
public static fromNode(node: ts.Node): StrictEqualAssertion | undefined {
|
||||
if (!ts.isCallExpression(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const expr = node.expression.getText();
|
||||
if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new StrictEqualAssertion(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the equals assertion at the given offset in the file.
|
||||
*/
|
||||
public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined {
|
||||
let node = findNodeAt(sf, offset);
|
||||
|
||||
while (node.parent) {
|
||||
const obj = StrictEqualAssertion.fromNode(node);
|
||||
if (obj) {
|
||||
return obj;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(private readonly expression: ts.CallExpression) { }
|
||||
|
||||
/** Gets the expected value */
|
||||
public get expectedValue(): ts.Expression | undefined {
|
||||
return this.expression.arguments[1];
|
||||
}
|
||||
|
||||
/** Gets the position of the assertion expression. */
|
||||
public get offsetStart(): number {
|
||||
return this.expression.getStart();
|
||||
}
|
||||
}
|
||||
|
||||
function findNodeAt(parent: ts.Node, offset: number): ts.Node {
|
||||
for (const child of parent.getChildren()) {
|
||||
if (child.getStart() <= offset && offset <= child.getEnd()) {
|
||||
return findNodeAt(child, offset);
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] {
|
||||
if (tests.testResults.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const run = tests.testResults[0];
|
||||
const snapshots = getTestResultsWithUri(run, uri);
|
||||
const result: TestMessage[] = [];
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
for (const m of snapshot.taskStates[0].messages) {
|
||||
if (
|
||||
m.location &&
|
||||
m.location.range.start.line <= lineNumber &&
|
||||
lineNumber <= m.location.range.end.line
|
||||
) {
|
||||
result.push(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): TestResultSnapshot[] {
|
||||
const results: TestResultSnapshot[] = [];
|
||||
|
||||
const walk = (r: TestResultSnapshot) => {
|
||||
for (const c of r.children) {
|
||||
walk(c);
|
||||
}
|
||||
if (r.uri?.toString() === uri.toString()) {
|
||||
results.push(r);
|
||||
}
|
||||
};
|
||||
|
||||
for (const r of testRun.results) {
|
||||
walk(r);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync, mkdirSync, renameSync } from 'fs';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { dirname, join } from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
interface IGitState {
|
||||
commitId: string;
|
||||
tracked: string;
|
||||
untracked: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ITrackedRemediation {
|
||||
snapshot: vscode.TestResultSnapshot;
|
||||
failing: IGitState;
|
||||
passing: IGitState;
|
||||
}
|
||||
|
||||
const MAX_FAILURES = 10;
|
||||
|
||||
export class FailureTracker {
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
private readonly lastFailed = new Map<
|
||||
string,
|
||||
{ snapshot: vscode.TestResultSnapshot; failing: IGitState }
|
||||
>();
|
||||
|
||||
private readonly logFile: string;
|
||||
private logs?: ITrackedRemediation[];
|
||||
|
||||
constructor(storageLocation: string, private readonly rootDir: string) {
|
||||
this.logFile = join(storageLocation, '.build/vscode-test-failures.json');
|
||||
mkdirSync(dirname(this.logFile), { recursive: true });
|
||||
|
||||
const oldLogFile = join(rootDir, '.build/vscode-test-failures.json');
|
||||
if (existsSync(oldLogFile)) {
|
||||
try {
|
||||
renameSync(oldLogFile, this.logFile);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand('selfhost-test-provider.openFailureLog', async () => {
|
||||
const doc = await vscode.workspace.openTextDocument(this.logFile);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
vscode.tests.onDidChangeTestResults(() => {
|
||||
const last = vscode.tests.testResults[0];
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
|
||||
let gitState: Promise<IGitState> | undefined;
|
||||
const getGitState = () => gitState ?? (gitState = this.captureGitState());
|
||||
|
||||
const queue = [last.results];
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
for (const snapshot of queue[i]) {
|
||||
// only interested in states of leaf tests
|
||||
if (snapshot.children.length) {
|
||||
queue.push(snapshot.children);
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = `${snapshot.uri}/${snapshot.id}`;
|
||||
const prev = this.lastFailed.get(key);
|
||||
if (snapshot.taskStates.some(s => s.state === vscode.TestResultState.Failed)) {
|
||||
// unset the parent to avoid a circular JSON structure:
|
||||
getGitState().then(s =>
|
||||
this.lastFailed.set(key, {
|
||||
snapshot: { ...snapshot, parent: undefined },
|
||||
failing: s,
|
||||
})
|
||||
);
|
||||
} else if (prev) {
|
||||
this.lastFailed.delete(key);
|
||||
getGitState().then(s => this.append({ ...prev, passing: s }));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async append(log: ITrackedRemediation) {
|
||||
if (!this.logs) {
|
||||
try {
|
||||
this.logs = JSON.parse(await readFile(this.logFile, 'utf-8'));
|
||||
} catch {
|
||||
this.logs = [];
|
||||
}
|
||||
}
|
||||
|
||||
const logs = this.logs!;
|
||||
logs.push(log);
|
||||
if (logs.length > MAX_FAILURES) {
|
||||
logs.splice(0, logs.length - MAX_FAILURES);
|
||||
}
|
||||
|
||||
await writeFile(this.logFile, JSON.stringify(logs, undefined, 2));
|
||||
}
|
||||
|
||||
private async captureGitState() {
|
||||
const [commitId, tracked, untracked] = await Promise.all([
|
||||
this.exec('git', ['rev-parse', 'HEAD']),
|
||||
this.exec('git', ['diff', 'HEAD']),
|
||||
this.exec('git', ['ls-files', '--others', '--exclude-standard']).then(async output => {
|
||||
const mapping: Record<string, string> = {};
|
||||
await Promise.all(
|
||||
output
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(async f => {
|
||||
mapping[f] = await readFile(join(this.rootDir, f), 'utf-8');
|
||||
})
|
||||
);
|
||||
return mapping;
|
||||
}),
|
||||
]);
|
||||
return { commitId, tracked, untracked };
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
private exec(command: string, args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { stdio: 'pipe', cwd: this.rootDir });
|
||||
let output = '';
|
||||
child.stdout.setEncoding('utf-8').on('data', b => (output += b));
|
||||
child.stderr.setEncoding('utf-8').on('data', b => (output += b));
|
||||
child.on('error', reject);
|
||||
child.on('exit', code =>
|
||||
code === 0
|
||||
? resolve(output)
|
||||
: reject(new Error(`Failed with error code ${code}\n${output}`))
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { bulkhead } from 'cockatiel';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const maxInt32 = 2 ** 31 - 1;
|
||||
|
||||
// limit concurrency to avoid overwhelming the filesystem during discovery
|
||||
const discoverLimiter = bulkhead(8, Infinity);
|
||||
|
||||
// Max import distance when listing related code to improve relevancy.
|
||||
const defaultMaxDistance = 3;
|
||||
|
||||
/**
|
||||
* Maintains a graph of imports in the codebase. This works lazily resolving
|
||||
* imports and re-parsing files only on request.
|
||||
*
|
||||
* This is a rough, file-level graph derived from simple regex matching on
|
||||
* source files to avoid having to parse the AST of every file in the codebase,
|
||||
* which is possible but more intensive. (See: all the years of work from the
|
||||
* TS language server.)
|
||||
*
|
||||
* A more advanced implementation could use references from the language server.
|
||||
*/
|
||||
export class ImportGraph implements vscode.TestRelatedCodeProvider {
|
||||
private graph = new Map<string, FileNode>();
|
||||
|
||||
constructor(
|
||||
private readonly root: vscode.Uri,
|
||||
private readonly discoverWorkspaceTests: () => Thenable<vscode.TestItem[]>,
|
||||
private readonly getTestNodeForDoc: (uri: vscode.Uri) => vscode.TestItem | undefined,
|
||||
) { }
|
||||
|
||||
/** @inheritdoc */
|
||||
public async provideRelatedCode(test: vscode.TestItem, token: vscode.CancellationToken): Promise<vscode.Location[]> {
|
||||
// this is kind of a stub for this implementation. Naive following imports
|
||||
// isn't that useful for finding a test's related code.
|
||||
const node = await this.discoverOutwards(test.uri, new Set(), defaultMaxDistance, token);
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const imports = new Set<string>();
|
||||
const queue = [{ distance: 0, next: node.imports }];
|
||||
while (queue.length) {
|
||||
const { distance, next } = queue.shift()!;
|
||||
for (const imp of next) {
|
||||
if (imports.has(imp.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
imports.add(imp.path);
|
||||
if (distance < defaultMaxDistance) {
|
||||
queue.push({ next: imp.imports, distance: distance + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...imports].map(importPath =>
|
||||
new vscode.Location(
|
||||
vscode.Uri.file(join(this.root.fsPath, 'src', `${importPath}.ts`)),
|
||||
new vscode.Range(0, 0, maxInt32, 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public async provideRelatedTests(document: vscode.TextDocument, _position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.TestItem[]> {
|
||||
// Expand all known tests to ensure imports of this file are realized.
|
||||
const rootTests = await this.discoverWorkspaceTests();
|
||||
const seen = new Set<string>();
|
||||
await Promise.all(rootTests.map(v => v.uri && this.discoverOutwards(v.uri, seen, defaultMaxDistance, token)));
|
||||
|
||||
const node = this.getNode(document.uri);
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tests: vscode.TestItem[] = [];
|
||||
const queue: { next: FileNode; distance: number }[] = [{ next: node, distance: 0 }];
|
||||
const visited = new Set<FileNode>();
|
||||
let maxDistance = Infinity;
|
||||
|
||||
while (queue.length) {
|
||||
const { next, distance } = queue.shift()!;
|
||||
if (visited.has(next)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.add(next);
|
||||
const testForDoc = this.getTestNodeForDoc(next.uri);
|
||||
if (testForDoc) {
|
||||
tests.push(testForDoc);
|
||||
// only look for tests half again as far away as the closest test to keep things relevant
|
||||
if (!Number.isFinite(maxDistance)) {
|
||||
maxDistance = distance * 3 / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (distance < maxDistance) {
|
||||
for (const importedByNode of next.importedBy) {
|
||||
queue.push({ next: importedByNode, distance: distance + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tests;
|
||||
}
|
||||
|
||||
public didChange(uri: vscode.Uri, deleted: boolean) {
|
||||
const rel = this.uriToImportPath(uri);
|
||||
const node = rel && this.graph.get(rel);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
this.graph.delete(rel);
|
||||
for (const imp of node.imports) {
|
||||
imp.importedBy.delete(node);
|
||||
}
|
||||
} else {
|
||||
node.isSynced = false;
|
||||
}
|
||||
}
|
||||
|
||||
private getNode(uri: vscode.Uri | undefined): FileNode | undefined {
|
||||
const rel = this.uriToImportPath(uri);
|
||||
return rel ? this.graph.get(rel) : undefined;
|
||||
}
|
||||
|
||||
/** Discover all nodes that import the file */
|
||||
private async discoverOutwards(uri: vscode.Uri | undefined, seen: Set<string>, maxDistance: number, token: vscode.CancellationToken): Promise<FileNode | undefined> {
|
||||
const rel = this.uriToImportPath(uri);
|
||||
if (!rel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let node = this.graph.get(rel);
|
||||
if (!node) {
|
||||
node = new FileNode(uri!, rel);
|
||||
this.graph.set(rel, node);
|
||||
}
|
||||
|
||||
await this.discoverOutwardsInner(node, seen, maxDistance, token);
|
||||
return node;
|
||||
}
|
||||
|
||||
private async discoverOutwardsInner(node: FileNode, seen: Set<string>, maxDistance: number, token: vscode.CancellationToken) {
|
||||
if (seen.has(node.path) || maxDistance === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
seen.add(node.path);
|
||||
if (node.isSynced === false) {
|
||||
await this.syncNode(node);
|
||||
} else if (node.isSynced instanceof Promise) {
|
||||
await node.isSynced;
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([...node.imports].map(i => this.discoverOutwardsInner(i, seen, maxDistance - 1, token)));
|
||||
}
|
||||
|
||||
private async syncNode(node: FileNode) {
|
||||
node.isSynced = discoverLimiter.execute(async () => {
|
||||
const doc = vscode.workspace.textDocuments.find(d => d.uri.toString() === node.uri.toString());
|
||||
|
||||
let text: string;
|
||||
if (doc) {
|
||||
text = doc.getText();
|
||||
} else {
|
||||
try {
|
||||
text = await fs.readFile(node.uri.fsPath, 'utf8');
|
||||
} catch {
|
||||
text = '';
|
||||
}
|
||||
}
|
||||
|
||||
for (const imp of node.imports) {
|
||||
imp.importedBy.delete(node);
|
||||
}
|
||||
node.imports.clear();
|
||||
|
||||
for (const [, importPath] of text.matchAll(IMPORT_RE)) {
|
||||
let imp = this.graph.get(importPath);
|
||||
if (!imp) {
|
||||
imp = new FileNode(this.importPathToUri(importPath), importPath);
|
||||
this.graph.set(importPath, imp);
|
||||
}
|
||||
|
||||
imp.importedBy.add(node);
|
||||
node.imports.add(imp);
|
||||
}
|
||||
|
||||
node.isSynced = true;
|
||||
});
|
||||
|
||||
await node.isSynced;
|
||||
}
|
||||
|
||||
private uriToImportPath(uri: vscode.Uri | undefined) {
|
||||
if (!uri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relativePath = vscode.workspace.asRelativePath(uri).replaceAll('\\', '/');
|
||||
if (!relativePath.startsWith('src/vs/') || !relativePath.endsWith('.ts')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return relativePath.slice('src/'.length, -'.ts'.length);
|
||||
}
|
||||
|
||||
private importPathToUri(importPath: string) {
|
||||
return vscode.Uri.file(join(this.root.fsPath, 'src', `${importPath}.ts`));
|
||||
}
|
||||
}
|
||||
|
||||
const IMPORT_RE = /import .*? from ["'](vs\/[^"']+)/g;
|
||||
|
||||
class FileNode {
|
||||
public imports = new Set<FileNode>();
|
||||
public importedBy = new Set<FileNode>();
|
||||
public isSynced: boolean | Promise<void> = false;
|
||||
|
||||
// Path is the *import path* starting with `vs/`
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
public readonly path: string,
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const memoizeLast = <A, T>(fn: (args: A) => T): ((args: A) => T) => {
|
||||
let last: { arg: A; result: T } | undefined;
|
||||
return arg => {
|
||||
if (last && last.arg === arg) {
|
||||
return last.result;
|
||||
}
|
||||
|
||||
const result = fn(arg);
|
||||
last = { arg, result };
|
||||
return result;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { TestMessage } from 'vscode';
|
||||
|
||||
export interface TestMessageMetadata {
|
||||
expectedValue: unknown;
|
||||
actualValue: unknown;
|
||||
}
|
||||
|
||||
const cache = new Array<{ id: string; metadata: TestMessageMetadata }>();
|
||||
|
||||
let id = 0;
|
||||
|
||||
function getId(): string {
|
||||
return `msg:${id++}:`;
|
||||
}
|
||||
|
||||
const regexp = /msg:\d+:/;
|
||||
|
||||
export function attachTestMessageMetadata(
|
||||
message: TestMessage,
|
||||
metadata: TestMessageMetadata
|
||||
): void {
|
||||
const existingMetadata = getTestMessageMetadata(message);
|
||||
if (existingMetadata) {
|
||||
Object.assign(existingMetadata, metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = getId();
|
||||
|
||||
if (typeof message.message === 'string') {
|
||||
message.message = `${message.message}\n${id}`;
|
||||
} else {
|
||||
message.message.appendText(`\n${id}`);
|
||||
}
|
||||
|
||||
cache.push({ id, metadata });
|
||||
while (cache.length > 100) {
|
||||
cache.shift();
|
||||
}
|
||||
}
|
||||
|
||||
export function getTestMessageMetadata(message: TestMessage): TestMessageMetadata | undefined {
|
||||
let value: string;
|
||||
if (typeof message.message === 'string') {
|
||||
value = message.message;
|
||||
} else {
|
||||
value = message.message.value;
|
||||
}
|
||||
|
||||
const result = regexp.exec(value);
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const id = result[0];
|
||||
return cache.find(c => c.id === id)?.metadata;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const snapshotComment = '\n\n// Snapshot file: ';
|
||||
|
||||
export const registerSnapshotUpdate = (ctrl: vscode.TestController) =>
|
||||
vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => {
|
||||
const message: vscode.TestMessage = args.message;
|
||||
const index = message.expectedOutput?.indexOf(snapshotComment);
|
||||
if (!message.expectedOutput || !message.actualOutput || !index || index === -1) {
|
||||
vscode.window.showErrorMessage('Could not find snapshot comment in message');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = message.expectedOutput.slice(index + snapshotComment.length);
|
||||
await fs.writeFile(file, message.actualOutput);
|
||||
ctrl.invalidateTestResults(args.test);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import * as vscode from 'vscode';
|
||||
import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree';
|
||||
|
||||
const suiteNames = new Set(['suite', 'flakySuite']);
|
||||
const testNames = new Set(['test']);
|
||||
|
||||
export const enum Action {
|
||||
Skip,
|
||||
Recurse,
|
||||
}
|
||||
|
||||
export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: VSCodeTest) => {
|
||||
if (!ts.isCallExpression(node)) {
|
||||
return Action.Recurse;
|
||||
}
|
||||
|
||||
const asSuite = identifyCall(node.expression, suiteNames);
|
||||
const asTest = identifyCall(node.expression, testNames);
|
||||
const either = asSuite || asTest;
|
||||
if (either === IdentifiedCall.Skipped) {
|
||||
return Action.Skip;
|
||||
}
|
||||
if (either === IdentifiedCall.Nothing) {
|
||||
return Action.Recurse;
|
||||
}
|
||||
|
||||
const name = node.arguments[0];
|
||||
const func = node.arguments[1];
|
||||
if (!name || !ts.isStringLiteralLike(name) || !func) {
|
||||
return Action.Recurse;
|
||||
}
|
||||
|
||||
const start = src.getLineAndCharacterOfPosition(name.pos);
|
||||
const end = src.getLineAndCharacterOfPosition(func.end);
|
||||
const range = new vscode.Range(
|
||||
new vscode.Position(start.line, start.character),
|
||||
new vscode.Position(end.line, end.character)
|
||||
);
|
||||
|
||||
const cparent = parent instanceof TestConstruct ? parent : undefined;
|
||||
|
||||
// we know this is either a suite or a test because we checked for skipped/nothing above
|
||||
|
||||
if (asTest) {
|
||||
return new TestCase(name.text, range, cparent);
|
||||
}
|
||||
|
||||
if (asSuite) {
|
||||
return new TestSuite(name.text, range, cparent);
|
||||
}
|
||||
|
||||
throw new Error('unreachable');
|
||||
};
|
||||
|
||||
const enum IdentifiedCall {
|
||||
Nothing,
|
||||
Skipped,
|
||||
IsThing,
|
||||
}
|
||||
|
||||
const identifyCall = (lhs: ts.Node, needles: ReadonlySet<string>): IdentifiedCall => {
|
||||
if (ts.isIdentifier(lhs)) {
|
||||
return needles.has(lhs.escapedText || lhs.text) ? IdentifiedCall.IsThing : IdentifiedCall.Nothing;
|
||||
}
|
||||
|
||||
if (isPropertyCall(lhs) && lhs.name.text === 'skip') {
|
||||
return needles.has(lhs.expression.text) ? IdentifiedCall.Skipped : IdentifiedCall.Nothing;
|
||||
}
|
||||
|
||||
if (ts.isParenthesizedExpression(lhs) && ts.isConditionalExpression(lhs.expression)) {
|
||||
return Math.max(identifyCall(lhs.expression.whenTrue, needles), identifyCall(lhs.expression.whenFalse, needles));
|
||||
}
|
||||
|
||||
return IdentifiedCall.Nothing;
|
||||
};
|
||||
|
||||
const isPropertyCall = (
|
||||
lhs: ts.Node
|
||||
): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } =>
|
||||
ts.isPropertyAccessExpression(lhs) &&
|
||||
ts.isIdentifier(lhs.expression) &&
|
||||
ts.isIdentifier(lhs.name);
|
||||
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Copied from https://github.com/microsoft/vscode-js-debug/blob/1d104b5184736677ab5cc280c70bbd227403850c/src/common/stackTraceParser.ts#L18
|
||||
|
||||
// Either match lines like
|
||||
// " at fulfilled (/Users/roblou/code/testapp-node2/out/app.js:5:58)"
|
||||
// or
|
||||
// " at /Users/roblou/code/testapp-node2/out/app.js:60:23"
|
||||
// and replace the path in them
|
||||
const re1 = /^(\W*at .*\()(.*):(\d+):(\d+)(\))$/;
|
||||
const re2 = /^(\W*at )(.*):(\d+):(\d+)$/;
|
||||
|
||||
const getLabelRe = /^\W*at (.*) \($/;
|
||||
|
||||
/**
|
||||
* Parses a textual stack trace.
|
||||
*/
|
||||
export class StackTraceParser {
|
||||
/** Gets whether the stacktrace has any locations in it. */
|
||||
public static isStackLike(str: string) {
|
||||
return re1.test(str) || re2.test(str);
|
||||
}
|
||||
constructor(private readonly stack: string) { }
|
||||
|
||||
/** Iterates over segments of text and locations in the stack. */
|
||||
*[Symbol.iterator]() {
|
||||
for (const line of this.stack.split('\n')) {
|
||||
const match = re1.exec(line) || re2.exec(line);
|
||||
if (!match) {
|
||||
yield line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, prefix, url, lineNo, columnNo, suffix] = match;
|
||||
if (prefix) {
|
||||
yield prefix;
|
||||
}
|
||||
|
||||
yield new StackTraceLocation(getLabelRe.exec(prefix)?.[1], url, Number(lineNo), Number(columnNo));
|
||||
|
||||
if (suffix) {
|
||||
yield suffix;
|
||||
}
|
||||
|
||||
yield '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StackTraceLocation {
|
||||
constructor(
|
||||
public readonly label: string | undefined,
|
||||
public readonly path: string,
|
||||
public readonly lineBase1: number,
|
||||
public readonly columnBase1: number,
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// DO NOT EDIT DIRECTLY: copied from src/vs/base/node/nodeStreams.ts
|
||||
|
||||
import { Transform } from 'stream';
|
||||
|
||||
/**
|
||||
* A Transform stream that splits the input on the "splitter" substring.
|
||||
* The resulting chunks will contain (and trail with) the splitter match.
|
||||
* The last chunk when the stream ends will be emitted even if a splitter
|
||||
* is not encountered.
|
||||
*/
|
||||
export class StreamSplitter extends Transform {
|
||||
private buffer: Buffer | undefined;
|
||||
private readonly splitter: number;
|
||||
private readonly spitterLen: number;
|
||||
|
||||
constructor(splitter: string | number | Buffer) {
|
||||
super();
|
||||
if (typeof splitter === 'number') {
|
||||
this.splitter = splitter;
|
||||
this.spitterLen = 1;
|
||||
} else {
|
||||
throw new Error('not implemented here');
|
||||
}
|
||||
}
|
||||
|
||||
override _transform(
|
||||
chunk: Buffer,
|
||||
_encoding: string,
|
||||
callback: (error?: Error | null, data?: any) => void
|
||||
): void {
|
||||
if (!this.buffer) {
|
||||
this.buffer = chunk;
|
||||
} else {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
while (offset < this.buffer.length) {
|
||||
const index = this.buffer.indexOf(this.splitter, offset);
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.push(this.buffer.slice(offset, index + this.spitterLen));
|
||||
offset = index + this.spitterLen;
|
||||
}
|
||||
|
||||
this.buffer = offset === this.buffer.length ? undefined : this.buffer.slice(offset);
|
||||
callback();
|
||||
}
|
||||
|
||||
override _flush(callback: (error?: Error | null, data?: any) => void): void {
|
||||
if (this.buffer) {
|
||||
this.push(this.buffer);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
decodedMappings,
|
||||
GREATEST_LOWER_BOUND,
|
||||
LEAST_UPPER_BOUND,
|
||||
originalPositionFor,
|
||||
TraceMap,
|
||||
} from '@jridgewell/trace-mapping';
|
||||
import * as styles from 'ansi-styles';
|
||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
||||
import * as vscode from 'vscode';
|
||||
import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider';
|
||||
import { attachTestMessageMetadata } from './metadata';
|
||||
import { snapshotComment } from './snapshot';
|
||||
import { StackTraceLocation, StackTraceParser } from './stackTraceParser';
|
||||
import { StreamSplitter } from './streamSplitter';
|
||||
import { getContentFromFilesystem } from './testTree';
|
||||
import { IScriptCoverage } from './v8CoverageWrangling';
|
||||
|
||||
export const enum MochaEvent {
|
||||
Start = 'start',
|
||||
TestStart = 'testStart',
|
||||
Pass = 'pass',
|
||||
Fail = 'fail',
|
||||
End = 'end',
|
||||
|
||||
// custom events:
|
||||
CoverageInit = 'coverageInit',
|
||||
CoverageIncrement = 'coverageIncrement',
|
||||
}
|
||||
|
||||
export interface IStartEvent {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ITestStartEvent {
|
||||
title: string;
|
||||
fullTitle: string;
|
||||
file: string;
|
||||
currentRetry: number;
|
||||
speed: string;
|
||||
}
|
||||
|
||||
export interface IPassEvent extends ITestStartEvent {
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface IFailEvent extends IPassEvent {
|
||||
err: string;
|
||||
stack: string | null;
|
||||
expected?: string;
|
||||
actual?: string;
|
||||
expectedJSON?: unknown;
|
||||
actualJSON?: unknown;
|
||||
snapshotPath?: string;
|
||||
}
|
||||
|
||||
export interface IEndEvent {
|
||||
suites: number;
|
||||
tests: number;
|
||||
passes: number;
|
||||
pending: number;
|
||||
failures: number;
|
||||
start: string /* ISO date */;
|
||||
end: string /* ISO date */;
|
||||
}
|
||||
|
||||
export interface ITestCoverageCoverage {
|
||||
file: string;
|
||||
fullTitle: string;
|
||||
coverage: { result: IScriptCoverage[] };
|
||||
}
|
||||
|
||||
export type MochaEventTuple =
|
||||
| [MochaEvent.Start, IStartEvent]
|
||||
| [MochaEvent.TestStart, ITestStartEvent]
|
||||
| [MochaEvent.Pass, IPassEvent]
|
||||
| [MochaEvent.Fail, IFailEvent]
|
||||
| [MochaEvent.End, IEndEvent]
|
||||
| [MochaEvent.CoverageInit, { result: IScriptCoverage[] }]
|
||||
| [MochaEvent.CoverageIncrement, ITestCoverageCoverage];
|
||||
|
||||
const LF = '\n'.charCodeAt(0);
|
||||
|
||||
export class TestOutputScanner implements vscode.Disposable {
|
||||
protected mochaEventEmitter = new vscode.EventEmitter<MochaEventTuple>();
|
||||
protected outputEventEmitter = new vscode.EventEmitter<string>();
|
||||
protected onExitEmitter = new vscode.EventEmitter<string | undefined>();
|
||||
|
||||
/**
|
||||
* Fired when a mocha event comes in.
|
||||
*/
|
||||
public readonly onMochaEvent = this.mochaEventEmitter.event;
|
||||
|
||||
/**
|
||||
* Fired when other output from the process comes in.
|
||||
*/
|
||||
public readonly onOtherOutput = this.outputEventEmitter.event;
|
||||
|
||||
/**
|
||||
* Fired when the process encounters an error, or exits.
|
||||
*/
|
||||
public readonly onRunnerExit = this.onExitEmitter.event;
|
||||
|
||||
constructor(private readonly process: ChildProcessWithoutNullStreams, private args?: string[]) {
|
||||
process.stdout.pipe(new StreamSplitter(LF)).on('data', this.processData);
|
||||
process.stderr.pipe(new StreamSplitter(LF)).on('data', this.processData);
|
||||
process.on('error', e => this.onExitEmitter.fire(e.message));
|
||||
process.on('exit', code =>
|
||||
this.onExitEmitter.fire(code ? `Test process exited with code ${code}` : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public dispose() {
|
||||
try {
|
||||
this.process.kill();
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly processData = (data: string | Buffer) => {
|
||||
if (this.args) {
|
||||
this.outputEventEmitter.fire(`./scripts/test ${this.args.join(' ')}`);
|
||||
this.args = undefined;
|
||||
}
|
||||
|
||||
data = data.toString();
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data.trim()) as unknown;
|
||||
if (parsed instanceof Array && parsed.length === 2 && typeof parsed[0] === 'string') {
|
||||
this.mochaEventEmitter.fire(parsed as MochaEventTuple);
|
||||
} else {
|
||||
this.outputEventEmitter.fire(data);
|
||||
}
|
||||
} catch {
|
||||
this.outputEventEmitter.fire(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type QueuedOutput = string | [string, vscode.Location | undefined, vscode.TestItem | undefined];
|
||||
|
||||
export async function scanTestOutput(
|
||||
tests: Map<string, vscode.TestItem>,
|
||||
task: vscode.TestRun,
|
||||
scanner: TestOutputScanner,
|
||||
coverageDir: string | undefined,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
const exitBlockers: Set<Promise<unknown>> = new Set();
|
||||
const skippedTests = new Set(tests.values());
|
||||
const store = new SourceMapStore();
|
||||
let outputQueue = Promise.resolve();
|
||||
const enqueueOutput = (fn: QueuedOutput | (() => Promise<QueuedOutput>)) => {
|
||||
exitBlockers.delete(outputQueue);
|
||||
outputQueue = outputQueue.finally(async () => {
|
||||
const r = typeof fn === 'function' ? await fn() : fn;
|
||||
typeof r === 'string' ? task.appendOutput(r) : task.appendOutput(...r);
|
||||
});
|
||||
exitBlockers.add(outputQueue);
|
||||
return outputQueue;
|
||||
};
|
||||
const enqueueExitBlocker = <T>(prom: Promise<T>): Promise<T> => {
|
||||
exitBlockers.add(prom);
|
||||
prom.finally(() => exitBlockers.delete(prom));
|
||||
return prom;
|
||||
};
|
||||
|
||||
let perTestCoverage: PerTestCoverageTracker | undefined;
|
||||
let lastTest: vscode.TestItem | undefined;
|
||||
let ranAnyTest = false;
|
||||
|
||||
try {
|
||||
if (cancellation.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
cancellation.onCancellationRequested(() => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
let currentTest: vscode.TestItem | undefined;
|
||||
|
||||
scanner.onRunnerExit(err => {
|
||||
if (err) {
|
||||
enqueueOutput(err + crlf);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
scanner.onOtherOutput(str => {
|
||||
const match = spdlogRe.exec(str);
|
||||
if (!match) {
|
||||
enqueueOutput(str + crlf);
|
||||
return;
|
||||
}
|
||||
|
||||
const logLocation = store.getSourceLocation(match[2], Number(match[3]) - 1);
|
||||
const logContents = replaceAllLocations(store, match[1]);
|
||||
const test = currentTest;
|
||||
|
||||
enqueueOutput(() =>
|
||||
Promise.all([logLocation, logContents]).then(([location, contents]) => [
|
||||
contents + crlf,
|
||||
location,
|
||||
test,
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
scanner.onMochaEvent(evt => {
|
||||
switch (evt[0]) {
|
||||
case MochaEvent.Start:
|
||||
break; // no-op
|
||||
case MochaEvent.TestStart:
|
||||
currentTest = tests.get(evt[1].fullTitle);
|
||||
if (!currentTest) {
|
||||
console.warn(`Could not find test ${evt[1].fullTitle}`);
|
||||
return;
|
||||
}
|
||||
skippedTests.delete(currentTest);
|
||||
task.started(currentTest);
|
||||
ranAnyTest = true;
|
||||
break;
|
||||
case MochaEvent.Pass:
|
||||
{
|
||||
const title = evt[1].fullTitle;
|
||||
const tcase = tests.get(title);
|
||||
enqueueOutput(` ${styles.green.open}√${styles.green.close} ${title}\r\n`);
|
||||
if (tcase) {
|
||||
lastTest = tcase;
|
||||
task.passed(tcase, evt[1].duration);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MochaEvent.Fail:
|
||||
{
|
||||
const {
|
||||
err,
|
||||
stack,
|
||||
duration,
|
||||
expected,
|
||||
expectedJSON,
|
||||
actual,
|
||||
actualJSON,
|
||||
snapshotPath,
|
||||
fullTitle: id,
|
||||
} = evt[1];
|
||||
let tcase = tests.get(id);
|
||||
// report failures on hook to the last-seen test, or first test if none run yet
|
||||
if (!tcase && (id.includes('hook for') || id.includes('hook in'))) {
|
||||
tcase = lastTest ?? tests.values().next().value;
|
||||
}
|
||||
|
||||
enqueueOutput(`${styles.red.open} x ${id}${styles.red.close}\r\n`);
|
||||
const rawErr = stack || err;
|
||||
const locationsReplaced = replaceAllLocations(store, forceCRLF(rawErr));
|
||||
if (rawErr) {
|
||||
enqueueOutput(async () => [await locationsReplaced, undefined, tcase]);
|
||||
}
|
||||
|
||||
if (!tcase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDiff =
|
||||
actual !== undefined &&
|
||||
expected !== undefined &&
|
||||
(expected !== '[undefined]' || actual !== '[undefined]');
|
||||
const testFirstLine =
|
||||
tcase.range &&
|
||||
new vscode.Location(
|
||||
tcase.uri!,
|
||||
new vscode.Range(
|
||||
tcase.range.start,
|
||||
new vscode.Position(tcase.range.start.line, 100)
|
||||
)
|
||||
);
|
||||
|
||||
enqueueExitBlocker(
|
||||
(async () => {
|
||||
const stackInfo = await deriveStackLocations(store, rawErr, tcase!);
|
||||
let message: vscode.TestMessage;
|
||||
|
||||
if (hasDiff) {
|
||||
message = new vscode.TestMessage(tryMakeMarkdown(err));
|
||||
message.actualOutput = outputToString(actual);
|
||||
message.expectedOutput = outputToString(expected);
|
||||
if (snapshotPath) {
|
||||
message.contextValue = 'isSelfhostSnapshotMessage';
|
||||
message.expectedOutput += snapshotComment + snapshotPath;
|
||||
}
|
||||
|
||||
attachTestMessageMetadata(message, {
|
||||
expectedValue: expectedJSON,
|
||||
actualValue: actualJSON,
|
||||
});
|
||||
} else {
|
||||
message = new vscode.TestMessage(
|
||||
stack ? await sourcemapStack(store, stack) : await locationsReplaced
|
||||
);
|
||||
}
|
||||
|
||||
message.location = stackInfo.primary ?? testFirstLine;
|
||||
message.stackTrace = stackInfo.stack;
|
||||
task.failed(tcase!, message, duration);
|
||||
})()
|
||||
);
|
||||
}
|
||||
break;
|
||||
case MochaEvent.End:
|
||||
// no-op, we wait until the process exits to ensure coverage is written out
|
||||
break;
|
||||
case MochaEvent.CoverageInit:
|
||||
perTestCoverage ??= new PerTestCoverageTracker(store);
|
||||
for (const result of evt[1].result) {
|
||||
perTestCoverage.add(result);
|
||||
}
|
||||
break;
|
||||
case MochaEvent.CoverageIncrement: {
|
||||
const { fullTitle, coverage } = evt[1];
|
||||
const tcase = tests.get(fullTitle);
|
||||
if (tcase) {
|
||||
perTestCoverage ??= new PerTestCoverageTracker(store);
|
||||
for (const result of coverage.result) {
|
||||
perTestCoverage.add(result, tcase);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (perTestCoverage) {
|
||||
enqueueExitBlocker(perTestCoverage.report(task));
|
||||
}
|
||||
|
||||
await Promise.all([...exitBlockers]);
|
||||
|
||||
if (coverageDir) {
|
||||
try {
|
||||
await istanbulCoverageContext.apply(task, coverageDir, {
|
||||
mapFileUri: uri => store.getSourceFile(uri.toString()),
|
||||
mapLocation: (uri, position) =>
|
||||
store.getSourceLocation(uri.toString(), position.line, position.character),
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = `Error loading coverage:\n\n${e}\n`;
|
||||
task.appendOutput(msg.replace(/\n/g, crlf));
|
||||
}
|
||||
}
|
||||
|
||||
// no tests? Possible crash, show output:
|
||||
if (!ranAnyTest) {
|
||||
await vscode.commands.executeCommand('testing.showMostRecentOutput');
|
||||
}
|
||||
} catch (e) {
|
||||
task.appendOutput((e as Error).stack || (e as Error).message);
|
||||
} finally {
|
||||
scanner.dispose();
|
||||
for (const test of skippedTests) {
|
||||
task.skipped(test);
|
||||
}
|
||||
task.end();
|
||||
}
|
||||
}
|
||||
|
||||
const spdlogRe = /"(.+)", source: (file:\/\/\/.*?)+ \(([0-9]+)\)/;
|
||||
const crlf = '\r\n';
|
||||
|
||||
const forceCRLF = (str: string) => str.replace(/(?<!\r)\n/gm, '\r\n');
|
||||
|
||||
const sourcemapStack = async (store: SourceMapStore, str: string) => {
|
||||
locationRe.lastIndex = 0;
|
||||
|
||||
const replacements = await Promise.all(
|
||||
[...str.matchAll(locationRe)].map(async match => {
|
||||
const location = await deriveSourceLocation(store, match);
|
||||
if (!location) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
from: match[0],
|
||||
to: location?.uri.with({
|
||||
fragment: `L${location.range.start.line + 1}:${location.range.start.character + 1}`,
|
||||
}),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const replacement of replacements) {
|
||||
if (replacement) {
|
||||
str = str.replace(replacement.from, replacement.to.toString(true));
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
const outputToString = (output: unknown) =>
|
||||
typeof output === 'object' ? JSON.stringify(output, null, 2) : String(output);
|
||||
|
||||
const tryMakeMarkdown = (message: string) => {
|
||||
const lines = message.split('\n');
|
||||
const start = lines.findIndex(l => l.includes('+ actual'));
|
||||
if (start === -1) {
|
||||
return message;
|
||||
}
|
||||
|
||||
lines.splice(start, 1, '```diff');
|
||||
lines.push('```');
|
||||
return new vscode.MarkdownString(lines.join('\n'));
|
||||
};
|
||||
|
||||
const inlineSourcemapRe = /^\/\/# sourceMappingURL=data:application\/json;base64,(.+)/m;
|
||||
const sourceMapBiases = [GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND] as const;
|
||||
|
||||
export const enum SearchStrategy {
|
||||
FirstBefore = -1,
|
||||
FirstAfter = 1,
|
||||
}
|
||||
|
||||
export type SourceLocationMapper = (line: number, col: number, strategy: SearchStrategy) => vscode.Location | undefined;
|
||||
|
||||
export class SourceMapStore {
|
||||
private readonly cache = new Map</* file uri */ string, Promise<TraceMap | undefined>>();
|
||||
|
||||
async getSourceLocationMapper(fileUri: string): Promise<SourceLocationMapper> {
|
||||
const sourceMap = await this.loadSourceMap(fileUri);
|
||||
return (line, col, strategy) => {
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 1. Look for the ideal position on this line if it exists
|
||||
const idealPosition = originalPositionFor(sourceMap, { column: col, line: line + 1, bias: SearchStrategy.FirstAfter ? GREATEST_LOWER_BOUND : LEAST_UPPER_BOUND });
|
||||
if (idealPosition.line !== null && idealPosition.column !== null && idealPosition.source !== null) {
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, idealPosition.source),
|
||||
new vscode.Position(idealPosition.line - 1, idealPosition.column)
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise get the first/last valid mapping on another line.
|
||||
const decoded = decodedMappings(sourceMap);
|
||||
const enum MapField {
|
||||
COLUMN = 0,
|
||||
SOURCES_INDEX = 1,
|
||||
SOURCE_LINE = 2,
|
||||
SOURCE_COLUMN = 3,
|
||||
}
|
||||
|
||||
do {
|
||||
line += strategy;
|
||||
const segments = decoded[line];
|
||||
if (!segments?.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = strategy === SearchStrategy.FirstBefore
|
||||
? findLastIndex(segments, s => s.length !== 1)
|
||||
: segments.findIndex(s => s.length !== 1);
|
||||
const segment = segments[index];
|
||||
|
||||
if (!segment || segment.length === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, sourceMap.sources[segment[MapField.SOURCES_INDEX]]!),
|
||||
new vscode.Position(segment[MapField.SOURCE_LINE] - 1, segment[MapField.SOURCE_COLUMN])
|
||||
);
|
||||
} while (strategy === SearchStrategy.FirstBefore ? line > 0 : line < decoded.length);
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/** Gets an original location from a base 0 line and column */
|
||||
async getSourceLocation(fileUri: string, line: number, col = 0) {
|
||||
const sourceMap = await this.loadSourceMap(fileUri);
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let smLine = line + 1;
|
||||
|
||||
// if the range is after the end of mappings, adjust it to the last mapped line
|
||||
const decoded = decodedMappings(sourceMap);
|
||||
if (decoded.length <= line) {
|
||||
smLine = decoded.length; // base 1, no -1 needed
|
||||
col = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
for (const bias of sourceMapBiases) {
|
||||
const position = originalPositionFor(sourceMap, { column: col, line: smLine, bias });
|
||||
if (position.line !== null && position.column !== null && position.source !== null) {
|
||||
return new vscode.Location(
|
||||
this.completeSourceMapUrl(sourceMap, position.source),
|
||||
new vscode.Position(position.line - 1, position.column)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getSourceFile(compiledUri: string) {
|
||||
const sourceMap = await this.loadSourceMap(compiledUri);
|
||||
if (!sourceMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (sourceMap.sources[0]) {
|
||||
return this.completeSourceMapUrl(sourceMap, sourceMap.sources[0]);
|
||||
}
|
||||
|
||||
for (const bias of sourceMapBiases) {
|
||||
const position = originalPositionFor(sourceMap, { column: 0, line: 1, bias });
|
||||
if (position.source !== null) {
|
||||
return this.completeSourceMapUrl(sourceMap, position.source);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private completeSourceMapUrl(sm: TraceMap, source: string) {
|
||||
if (sm.sourceRoot) {
|
||||
try {
|
||||
return vscode.Uri.parse(new URL(source, sm.sourceRoot).toString());
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return vscode.Uri.parse(source);
|
||||
}
|
||||
|
||||
private loadSourceMap(fileUri: string) {
|
||||
const existing = this.cache.get(fileUri);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const contents = await getContentFromFilesystem(vscode.Uri.parse(fileUri));
|
||||
const sourcemapMatch = inlineSourcemapRe.exec(contents);
|
||||
if (!sourcemapMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = Buffer.from(sourcemapMatch[1], 'base64').toString();
|
||||
return new TraceMap(decoded, fileUri);
|
||||
} catch (e) {
|
||||
console.warn(`Error parsing sourcemap for ${fileUri}: ${(e as Error).stack}`);
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
this.cache.set(fileUri, promise);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
const locationRe = /(file:\/{3}.+):([0-9]+):([0-9]+)/g;
|
||||
|
||||
async function replaceAllLocations(store: SourceMapStore, str: string) {
|
||||
const output: (string | Promise<string>)[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of str.matchAll(locationRe)) {
|
||||
const locationPromise = deriveSourceLocation(store, match);
|
||||
const startIndex = match.index || 0;
|
||||
const endIndex = startIndex + match[0].length;
|
||||
|
||||
if (startIndex > lastIndex) {
|
||||
output.push(str.substring(lastIndex, startIndex));
|
||||
}
|
||||
|
||||
output.push(
|
||||
locationPromise.then(location =>
|
||||
location
|
||||
? `${location.uri}:${location.range.start.line + 1}:${location.range.start.character + 1}`
|
||||
: match[0]
|
||||
)
|
||||
);
|
||||
|
||||
lastIndex = endIndex;
|
||||
}
|
||||
|
||||
// Preserve the remaining string after the last match
|
||||
if (lastIndex < str.length) {
|
||||
output.push(str.substring(lastIndex));
|
||||
}
|
||||
|
||||
const values = await Promise.all(output);
|
||||
return values.join('');
|
||||
}
|
||||
|
||||
async function deriveStackLocations(
|
||||
store: SourceMapStore,
|
||||
stack: string,
|
||||
tcase: vscode.TestItem
|
||||
) {
|
||||
locationRe.lastIndex = 0;
|
||||
|
||||
const locationsRaw = [...new StackTraceParser(stack)].filter(t => t instanceof StackTraceLocation);
|
||||
const locationsMapped = await Promise.all(locationsRaw.map(async location => {
|
||||
const mapped = location.path.startsWith('file:') ? await store.getSourceLocation(location.path, location.lineBase1 - 1, location.columnBase1 - 1) : undefined;
|
||||
const stack = new vscode.TestMessageStackFrame(location.label || '<anonymous>', mapped?.uri, mapped?.range.start || new vscode.Position(location.lineBase1 - 1, location.columnBase1 - 1));
|
||||
return { location: mapped, stack };
|
||||
}));
|
||||
|
||||
let best: undefined | { location: vscode.Location; score: number };
|
||||
for (const { location } of locationsMapped) {
|
||||
if (!location) {
|
||||
continue;
|
||||
}
|
||||
let score = 0;
|
||||
if (tcase.uri && tcase.uri.toString() === location.uri.toString()) {
|
||||
score = 1;
|
||||
if (tcase.range && tcase.range.contains(location?.range)) {
|
||||
score = 2;
|
||||
}
|
||||
}
|
||||
if (!best || score > best.score) {
|
||||
best = { location, score };
|
||||
}
|
||||
}
|
||||
|
||||
return { stack: locationsMapped.map(s => s.stack), primary: best?.location };
|
||||
}
|
||||
|
||||
async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) {
|
||||
const [, fileUri, line, col] = parts;
|
||||
return store.getSourceLocation(fileUri, Number(line) - 1, Number(col));
|
||||
}
|
||||
|
||||
function findLastIndex<T>(arr: T[], predicate: (value: T) => boolean) {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (predicate(arr[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join, relative } from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import { TextDecoder } from 'util';
|
||||
import * as vscode from 'vscode';
|
||||
import { Action, extractTestFromNode } from './sourceUtils';
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('selfhostTestProvider');
|
||||
|
||||
type ContentGetter = (uri: vscode.Uri) => Promise<string>;
|
||||
|
||||
export const itemData = new WeakMap<vscode.TestItem, VSCodeTest>();
|
||||
|
||||
export const clearFileDiagnostics = (uri: vscode.Uri) => diagnosticCollection.delete(uri);
|
||||
|
||||
/**
|
||||
* Tries to guess which workspace folder VS Code is in.
|
||||
*/
|
||||
export const guessWorkspaceFolder = async () => {
|
||||
if (!vscode.workspace.workspaceFolders) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (vscode.workspace.workspaceFolders.length < 2) {
|
||||
return vscode.workspace.workspaceFolders[0];
|
||||
}
|
||||
|
||||
for (const folder of vscode.workspace.workspaceFolders) {
|
||||
try {
|
||||
await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js'));
|
||||
return folder;
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getContentFromFilesystem: ContentGetter = async uri => {
|
||||
try {
|
||||
const rawContent = await vscode.workspace.fs.readFile(uri);
|
||||
return textDecoder.decode(rawContent);
|
||||
} catch (e) {
|
||||
console.warn(`Error providing tests for ${uri.fsPath}`, e);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export class TestFile {
|
||||
public hasBeenRead = false;
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
public readonly workspaceFolder: vscode.WorkspaceFolder
|
||||
) {}
|
||||
|
||||
public getId() {
|
||||
return this.uri.toString().toLowerCase();
|
||||
}
|
||||
|
||||
public getLabel() {
|
||||
return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath);
|
||||
}
|
||||
|
||||
public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) {
|
||||
try {
|
||||
const content = await getContentFromFilesystem(item.uri!);
|
||||
item.error = undefined;
|
||||
this.updateFromContents(controller, content, item);
|
||||
} catch (e) {
|
||||
item.error = (e as Error).stack;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all tests in this file, `sourceReader` provided by the root.
|
||||
*/
|
||||
public updateFromContents(
|
||||
controller: vscode.TestController,
|
||||
content: string,
|
||||
file: vscode.TestItem
|
||||
) {
|
||||
try {
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
const ast = ts.createSourceFile(
|
||||
this.uri.path.split('/').pop()!,
|
||||
content,
|
||||
ts.ScriptTarget.ESNext,
|
||||
false,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
|
||||
const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [
|
||||
{ item: file, children: [] },
|
||||
];
|
||||
const traverse = (node: ts.Node) => {
|
||||
const parent = parents[parents.length - 1];
|
||||
const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!);
|
||||
if (childData === Action.Skip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (childData === Action.Recurse) {
|
||||
ts.forEachChild(node, traverse);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = `${file.uri}/${childData.fullName}`.toLowerCase();
|
||||
|
||||
// Skip duplicated tests. They won't run correctly with the way
|
||||
// mocha reports them, and will error if we try to insert them.
|
||||
const existing = parent.children.find(c => c.id === id);
|
||||
if (existing) {
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
childData.range,
|
||||
'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.',
|
||||
vscode.DiagnosticSeverity.Warning
|
||||
);
|
||||
|
||||
diagnostic.relatedInformation = [
|
||||
new vscode.DiagnosticRelatedInformation(
|
||||
new vscode.Location(existing.uri!, existing.range!),
|
||||
'First declared here'
|
||||
),
|
||||
];
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
return;
|
||||
}
|
||||
|
||||
const item = controller.createTestItem(id, childData.name, file.uri);
|
||||
itemData.set(item, childData);
|
||||
item.range = childData.range;
|
||||
parent.children.push(item);
|
||||
|
||||
if (childData instanceof TestSuite) {
|
||||
parents.push({ item: item, children: [] });
|
||||
ts.forEachChild(node, traverse);
|
||||
item.children.replace(parents.pop()!.children);
|
||||
}
|
||||
};
|
||||
|
||||
ts.forEachChild(ast, traverse);
|
||||
file.error = undefined;
|
||||
file.children.replace(parents[0].children);
|
||||
diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined);
|
||||
this.hasBeenRead = true;
|
||||
} catch (e) {
|
||||
file.error = String((e as Error).stack || (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class TestConstruct {
|
||||
public fullName: string;
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly range: vscode.Range,
|
||||
parent?: TestConstruct
|
||||
) {
|
||||
this.fullName = parent ? `${parent.fullName} ${name}` : name;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestSuite extends TestConstruct {}
|
||||
|
||||
export class TestCase extends TestConstruct {}
|
||||
|
||||
export type VSCodeTest = TestFile | TestSuite | TestCase;
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { RangeCoverageTracker } from './v8CoverageWrangling';
|
||||
|
||||
suite('v8CoverageWrangling', () => {
|
||||
suite('RangeCoverageTracker', () => {
|
||||
test('covers new range', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.cover(5, 10);
|
||||
assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]);
|
||||
});
|
||||
|
||||
test('non overlapping ranges', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.cover(5, 10);
|
||||
rt.cover(15, 20);
|
||||
rt.cover(12, 13);
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 5, end: 10, covered: true },
|
||||
{ start: 12, end: 13, covered: true },
|
||||
{ start: 15, end: 20, covered: true },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('covers exact', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.uncovered(5, 10);
|
||||
rt.cover(5, 10);
|
||||
assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]);
|
||||
});
|
||||
|
||||
test('overlap at start', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.uncovered(5, 10);
|
||||
rt.cover(2, 7);
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 2, end: 7, covered: true },
|
||||
{ start: 7, end: 10, covered: false },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('overlap at end', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.cover(5, 10);
|
||||
rt.uncovered(2, 7);
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 2, end: 5, covered: false },
|
||||
{ start: 5, end: 10, covered: true },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('inner contained', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.cover(5, 10);
|
||||
rt.uncovered(2, 12);
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 2, end: 5, covered: false },
|
||||
{ start: 5, end: 10, covered: true },
|
||||
{ start: 10, end: 12, covered: false },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('outer contained', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.uncovered(5, 10);
|
||||
rt.cover(7, 9);
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 5, end: 7, covered: false },
|
||||
{ start: 7, end: 9, covered: true },
|
||||
{ start: 9, end: 10, covered: false },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('boundary touching', () => {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.uncovered(5, 10);
|
||||
rt.cover(10, 15);
|
||||
rt.uncovered(15, 20);
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 5, end: 10, covered: false },
|
||||
{ start: 10, end: 15, covered: true },
|
||||
{ start: 15, end: 20, covered: false },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
suite('initializeBlock', () => {
|
||||
test('simple tree', () => {
|
||||
const rt = RangeCoverageTracker.initializeBlocks([
|
||||
{
|
||||
functionName: 'outer',
|
||||
isBlockCoverage: true,
|
||||
ranges: [
|
||||
{ count: 1, startOffset: 5, endOffset: 30 },
|
||||
{ count: 1, startOffset: 8, endOffset: 10 },
|
||||
{ count: 0, startOffset: 15, endOffset: 20 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 5, end: 15, covered: true },
|
||||
{ start: 15, end: 20, covered: false },
|
||||
{ start: 20, end: 30, covered: true },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('separate branches', () => {
|
||||
const rt = RangeCoverageTracker.initializeBlocks([
|
||||
{
|
||||
functionName: 'outer',
|
||||
isBlockCoverage: true,
|
||||
ranges: [
|
||||
{ count: 1, startOffset: 5, endOffset: 8 },
|
||||
{ count: 1, startOffset: 10, endOffset: 12 },
|
||||
{ count: 0, startOffset: 15, endOffset: 20 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
[...rt],
|
||||
[
|
||||
{ start: 5, end: 8, covered: true },
|
||||
{ start: 10, end: 12, covered: true },
|
||||
{ start: 15, end: 20, covered: false },
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ICoverageRange {
|
||||
start: number;
|
||||
end: number;
|
||||
covered: boolean;
|
||||
}
|
||||
|
||||
export interface IV8FunctionCoverage {
|
||||
functionName: string;
|
||||
isBlockCoverage: boolean;
|
||||
ranges: IV8CoverageRange[];
|
||||
}
|
||||
|
||||
export interface IV8CoverageRange {
|
||||
startOffset: number;
|
||||
endOffset: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** V8 Script coverage data */
|
||||
export interface IScriptCoverage {
|
||||
scriptId: string;
|
||||
url: string;
|
||||
// Script source added by the runner the first time the script is emitted.
|
||||
source?: string;
|
||||
functions: IV8FunctionCoverage[];
|
||||
}
|
||||
|
||||
export class RangeCoverageTracker implements Iterable<ICoverageRange> {
|
||||
/**
|
||||
* A noncontiguous, non-overlapping, ordered set of ranges and whether
|
||||
* that range has been covered.
|
||||
*/
|
||||
private ranges: readonly ICoverageRange[] = [];
|
||||
|
||||
/**
|
||||
* Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true.
|
||||
*/
|
||||
public static initializeBlocks(fns: IV8FunctionCoverage[]) {
|
||||
const rt = new RangeCoverageTracker();
|
||||
|
||||
let start = 0;
|
||||
const stack: IV8CoverageRange[] = [];
|
||||
|
||||
// note: comes pre-sorted from V8
|
||||
for (const { ranges } of fns) {
|
||||
for (const range of ranges) {
|
||||
while (stack.length && stack[stack.length - 1].endOffset < range.startOffset) {
|
||||
const last = stack.pop()!;
|
||||
rt.setCovered(start, last.endOffset, last.count > 0);
|
||||
start = last.endOffset;
|
||||
}
|
||||
|
||||
if (range.startOffset > start && stack.length) {
|
||||
rt.setCovered(start, range.startOffset, !!stack[stack.length - 1].count);
|
||||
}
|
||||
|
||||
start = range.startOffset;
|
||||
stack.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
while (stack.length) {
|
||||
const last = stack.pop()!;
|
||||
rt.setCovered(start, last.endOffset, last.count > 0);
|
||||
start = last.endOffset;
|
||||
}
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
/** Makes a copy of the range tracker. */
|
||||
public clone() {
|
||||
const rt = new RangeCoverageTracker();
|
||||
rt.ranges = this.ranges.slice();
|
||||
return rt;
|
||||
}
|
||||
|
||||
/** Marks a range covered */
|
||||
public cover(start: number, end: number) {
|
||||
this.setCovered(start, end, true);
|
||||
}
|
||||
|
||||
/** Marks a range as uncovered */
|
||||
public uncovered(start: number, end: number) {
|
||||
this.setCovered(start, end, false);
|
||||
}
|
||||
|
||||
/** Iterates over coverage ranges */
|
||||
[Symbol.iterator]() {
|
||||
return this.ranges[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given character range as being covered or uncovered.
|
||||
*
|
||||
* todo@connor4312: this is a hot path is could probably be optimized to
|
||||
* avoid rebuilding the array. Maybe with a nice tree structure?
|
||||
*/
|
||||
public setCovered(start: number, end: number, covered: boolean) {
|
||||
const newRanges: ICoverageRange[] = [];
|
||||
let i = 0;
|
||||
for (; i < this.ranges.length && this.ranges[i].end <= start; i++) {
|
||||
newRanges.push(this.ranges[i]);
|
||||
}
|
||||
|
||||
const push = (range: ICoverageRange) => {
|
||||
const last = newRanges.length && newRanges[newRanges.length - 1];
|
||||
if (last && last.end === range.start && last.covered === range.covered) {
|
||||
last.end = range.end;
|
||||
} else {
|
||||
newRanges.push(range);
|
||||
}
|
||||
};
|
||||
|
||||
push({ start, end, covered });
|
||||
|
||||
for (; i < this.ranges.length; i++) {
|
||||
const range = this.ranges[i];
|
||||
const last = newRanges[newRanges.length - 1];
|
||||
|
||||
if (range.start === last.start && range.end === last.end) {
|
||||
// ranges are equal:
|
||||
last.covered ||= range.covered;
|
||||
} else if (range.end < last.start || range.start > last.end) {
|
||||
// ranges don't overlap
|
||||
push(range);
|
||||
} else if (range.start < last.start && range.end > last.end) {
|
||||
// range contains last:
|
||||
newRanges.pop();
|
||||
push({ start: range.start, end: last.start, covered: range.covered });
|
||||
push({ start: last.start, end: last.end, covered: range.covered || last.covered });
|
||||
push({ start: last.end, end: range.end, covered: range.covered });
|
||||
} else if (range.start >= last.start && range.end <= last.end) {
|
||||
// last contains range:
|
||||
newRanges.pop();
|
||||
push({ start: last.start, end: range.start, covered: last.covered });
|
||||
push({ start: range.start, end: range.end, covered: range.covered || last.covered });
|
||||
push({ start: range.end, end: last.end, covered: last.covered });
|
||||
} else if (range.start < last.start && range.end <= last.end) {
|
||||
// range overlaps start of last:
|
||||
newRanges.pop();
|
||||
push({ start: range.start, end: last.start, covered: range.covered });
|
||||
push({ start: last.start, end: range.end, covered: range.covered || last.covered });
|
||||
push({ start: range.end, end: last.end, covered: last.covered });
|
||||
} else if (range.start >= last.start && range.end > last.end) {
|
||||
// range overlaps end of last:
|
||||
newRanges.pop();
|
||||
push({ start: last.start, end: range.start, covered: last.covered });
|
||||
push({ start: range.start, end: last.end, covered: range.covered || last.covered });
|
||||
push({ start: last.end, end: range.end, covered: range.covered });
|
||||
} else {
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
}
|
||||
|
||||
this.ranges = newRanges;
|
||||
}
|
||||
}
|
||||
|
||||
export class OffsetToPosition {
|
||||
/** Line numbers to byte offsets. */
|
||||
public readonly lines: number[] = [];
|
||||
|
||||
public readonly totalLength: number;
|
||||
|
||||
constructor(source: string) {
|
||||
this.lines.push(0);
|
||||
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) {
|
||||
this.lines.push(i + 1);
|
||||
}
|
||||
this.totalLength = source.length;
|
||||
}
|
||||
|
||||
public getLineLength(lineNumber: number): number {
|
||||
return (
|
||||
(lineNumber < this.lines.length - 1 ? this.lines[lineNumber + 1] - 1 : this.totalLength) -
|
||||
this.lines[lineNumber]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the line the offset appears on.
|
||||
*/
|
||||
public getLineOfOffset(offset: number): number {
|
||||
let low = 0;
|
||||
let high = this.lines.length;
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (this.lines[mid] > offset) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return low - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from a file offset to a base 0 line/column .
|
||||
*/
|
||||
public toLineColumn(offset: number): { line: number; column: number } {
|
||||
const line = this.getLineOfOffset(offset);
|
||||
return { line: line, column: offset - this.lines[line] };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import { AddressInfo, createServer } from 'net';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { TestOutputScanner } from './testOutputScanner';
|
||||
import { TestCase, TestFile, TestSuite, itemData } from './testTree';
|
||||
|
||||
/**
|
||||
* From MDN
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
|
||||
*/
|
||||
const escapeRe = (s: string) => s.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
const TEST_ELECTRON_SCRIPT_PATH = 'test/unit/electron/index.js';
|
||||
const TEST_BROWSER_SCRIPT_PATH = 'test/unit/browser/index.js';
|
||||
|
||||
const ATTACH_CONFIG_NAME = 'Attach to VS Code';
|
||||
const DEBUG_TYPE = 'pwa-chrome';
|
||||
|
||||
export abstract class VSCodeTestRunner {
|
||||
constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { }
|
||||
|
||||
public async run(baseArgs: ReadonlyArray<string>, filter?: ReadonlyArray<vscode.TestItem>) {
|
||||
const args = this.prepareArguments(baseArgs, filter);
|
||||
const cp = spawn(await this.binaryPath(), args, {
|
||||
cwd: this.repoLocation.uri.fsPath,
|
||||
stdio: 'pipe',
|
||||
env: this.getEnvironment(),
|
||||
});
|
||||
|
||||
return new TestOutputScanner(cp, args);
|
||||
}
|
||||
|
||||
public async debug(testRun: vscode.TestRun, baseArgs: ReadonlyArray<string>, filter?: ReadonlyArray<vscode.TestItem>) {
|
||||
const port = await this.findOpenPort();
|
||||
const baseConfiguration = vscode.workspace
|
||||
.getConfiguration('launch', this.repoLocation)
|
||||
.get<vscode.DebugConfiguration[]>('configurations', [])
|
||||
.find(c => c.name === ATTACH_CONFIG_NAME);
|
||||
|
||||
if (!baseConfiguration) {
|
||||
throw new Error(`Could not find launch configuration ${ATTACH_CONFIG_NAME}`);
|
||||
}
|
||||
|
||||
const server = this.createWaitServer();
|
||||
const args = [
|
||||
...this.prepareArguments(baseArgs, filter),
|
||||
`--remote-debugging-port=${port}`,
|
||||
// for breakpoint freeze: https://github.com/microsoft/vscode/issues/122225#issuecomment-885377304
|
||||
'--js-flags="--regexp_interpret_all"',
|
||||
// for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910
|
||||
'--disable-features=CalculateNativeWinOcclusion',
|
||||
'--timeout=0',
|
||||
`--waitServer=${server.port}`,
|
||||
];
|
||||
|
||||
const cp = spawn(await this.binaryPath(), args, {
|
||||
cwd: this.repoLocation.uri.fsPath,
|
||||
stdio: 'pipe',
|
||||
env: this.getEnvironment(port),
|
||||
});
|
||||
|
||||
// Register a descriptor factory that signals the server when any
|
||||
// breakpoint set requests on the debugee have been completed.
|
||||
const factory = vscode.debug.registerDebugAdapterTrackerFactory(DEBUG_TYPE, {
|
||||
createDebugAdapterTracker(session) {
|
||||
if (!session.parentSession || session.parentSession !== rootSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
let initRequestId: number | undefined;
|
||||
|
||||
return {
|
||||
onDidSendMessage(message) {
|
||||
if (message.type === 'response' && message.request_seq === initRequestId) {
|
||||
server.ready();
|
||||
}
|
||||
},
|
||||
onWillReceiveMessage(message) {
|
||||
if (initRequestId !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.command === 'launch' || message.command === 'attach') {
|
||||
initRequestId = message.seq;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
vscode.debug.startDebugging(this.repoLocation, { ...baseConfiguration, port }, { testRun });
|
||||
|
||||
let exited = false;
|
||||
let rootSession: vscode.DebugSession | undefined;
|
||||
cp.once('exit', () => {
|
||||
exited = true;
|
||||
server.dispose();
|
||||
listener.dispose();
|
||||
factory.dispose();
|
||||
|
||||
if (rootSession) {
|
||||
vscode.debug.stopDebugging(rootSession);
|
||||
}
|
||||
});
|
||||
|
||||
const listener = vscode.debug.onDidStartDebugSession(s => {
|
||||
if (s.name === ATTACH_CONFIG_NAME && !rootSession) {
|
||||
if (exited) {
|
||||
vscode.debug.stopDebugging(rootSession);
|
||||
} else {
|
||||
rootSession = s;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new TestOutputScanner(cp, args);
|
||||
}
|
||||
|
||||
private findOpenPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.listen(0, () => {
|
||||
const address = server.address() as AddressInfo;
|
||||
const port = address.port;
|
||||
server.close(() => {
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
server.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getEnvironment(_remoteDebugPort?: number): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: undefined,
|
||||
ELECTRON_ENABLE_LOGGING: '1',
|
||||
};
|
||||
}
|
||||
|
||||
private prepareArguments(
|
||||
baseArgs: ReadonlyArray<string>,
|
||||
filter?: ReadonlyArray<vscode.TestItem>
|
||||
) {
|
||||
const args = [...this.getDefaultArgs(), ...baseArgs, '--reporter', 'full-json-stream'];
|
||||
if (!filter) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const grepRe: string[] = [];
|
||||
const runPaths = new Set<string>();
|
||||
const addTestFileRunPath = (data: TestFile) =>
|
||||
runPaths.add(
|
||||
path.relative(data.workspaceFolder.uri.fsPath, data.uri.fsPath).replace(/\\/g, '/')
|
||||
);
|
||||
|
||||
const itemDatas = filter.map(f => itemData.get(f));
|
||||
/** If true, we have to be careful with greps, as a grep for one test file affects the run of the other test file. */
|
||||
const hasBothTestCaseOrTestSuiteAndTestFileFilters =
|
||||
itemDatas.some(d => (d instanceof TestCase) || (d instanceof TestSuite)) &&
|
||||
itemDatas.some(d => d instanceof TestFile);
|
||||
|
||||
function addTestCaseOrSuite(data: TestCase | TestSuite, test: vscode.TestItem): void {
|
||||
grepRe.push(escapeRe(data.fullName) + (data instanceof TestCase ? '$' : ' '));
|
||||
for (let p = test.parent; p; p = p.parent) {
|
||||
const parentData = itemData.get(p);
|
||||
if (parentData instanceof TestFile) {
|
||||
addTestFileRunPath(parentData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const test of filter) {
|
||||
const data = itemData.get(test);
|
||||
if (data instanceof TestCase || data instanceof TestSuite) {
|
||||
addTestCaseOrSuite(data, test);
|
||||
} else if (data instanceof TestFile) {
|
||||
if (!hasBothTestCaseOrTestSuiteAndTestFileFilters) {
|
||||
addTestFileRunPath(data);
|
||||
} else {
|
||||
// We add all the items individually so they get their own grep expressions.
|
||||
for (const [_id, nestedTest] of test.children) {
|
||||
const childData = itemData.get(nestedTest);
|
||||
if (childData instanceof TestCase || childData instanceof TestSuite) {
|
||||
addTestCaseOrSuite(childData, nestedTest);
|
||||
} else {
|
||||
console.error('Unexpected test item in test file', nestedTest.id, nestedTest.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (grepRe.length) {
|
||||
args.push('--grep', `/^(${grepRe.join('|')})/`);
|
||||
}
|
||||
|
||||
if (runPaths.size) {
|
||||
args.push(...[...runPaths].flatMap(p => ['--run', p]));
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected abstract getDefaultArgs(): string[];
|
||||
|
||||
protected abstract binaryPath(): Promise<string>;
|
||||
|
||||
protected async readProductJson() {
|
||||
const projectJson = await fs.readFile(
|
||||
path.join(this.repoLocation.uri.fsPath, 'product.json'),
|
||||
'utf-8'
|
||||
);
|
||||
try {
|
||||
return JSON.parse(projectJson);
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing product.json: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private createWaitServer() {
|
||||
const onReady = new vscode.EventEmitter<void>();
|
||||
let ready = false;
|
||||
|
||||
const server = createServer(socket => {
|
||||
if (ready) {
|
||||
socket.end();
|
||||
} else {
|
||||
onReady.event(() => socket.end());
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(0);
|
||||
|
||||
return {
|
||||
port: (server.address() as AddressInfo).port,
|
||||
ready: () => {
|
||||
ready = true;
|
||||
onReady.fire();
|
||||
},
|
||||
dispose: () => {
|
||||
server.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class BrowserTestRunner extends VSCodeTestRunner {
|
||||
/** @override */
|
||||
protected binaryPath(): Promise<string> {
|
||||
return Promise.resolve(process.execPath);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
protected override getEnvironment(remoteDebugPort?: number) {
|
||||
return {
|
||||
...super.getEnvironment(remoteDebugPort),
|
||||
PLAYWRIGHT_CHROMIUM_DEBUG_PORT: remoteDebugPort ? String(remoteDebugPort) : undefined,
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
protected getDefaultArgs() {
|
||||
return [TEST_BROWSER_SCRIPT_PATH];
|
||||
}
|
||||
}
|
||||
|
||||
export class WindowsTestRunner extends VSCodeTestRunner {
|
||||
/** @override */
|
||||
protected async binaryPath() {
|
||||
const { nameShort } = await this.readProductJson();
|
||||
return path.join(this.repoLocation.uri.fsPath, `.build/electron/${nameShort}.exe`);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
protected getDefaultArgs() {
|
||||
return [TEST_ELECTRON_SCRIPT_PATH];
|
||||
}
|
||||
}
|
||||
|
||||
export class PosixTestRunner extends VSCodeTestRunner {
|
||||
/** @override */
|
||||
protected async binaryPath() {
|
||||
const { applicationName } = await this.readProductJson();
|
||||
return path.join(this.repoLocation.uri.fsPath, `.build/electron/${applicationName}`);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
protected getDefaultArgs() {
|
||||
return [TEST_ELECTRON_SCRIPT_PATH];
|
||||
}
|
||||
}
|
||||
|
||||
export class DarwinTestRunner extends PosixTestRunner {
|
||||
/** @override */
|
||||
protected override getDefaultArgs() {
|
||||
return [
|
||||
TEST_ELECTRON_SCRIPT_PATH,
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--use-gl=swiftshader',
|
||||
];
|
||||
}
|
||||
|
||||
/** @override */
|
||||
protected override async binaryPath() {
|
||||
const { nameLong } = await this.readProductJson();
|
||||
return path.join(
|
||||
this.repoLocation.uri.fsPath,
|
||||
`.build/electron/${nameLong}.app/Contents/MacOS/Electron`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PlatformTestRunner =
|
||||
process.platform === 'win32'
|
||||
? WindowsTestRunner
|
||||
: process.platform === 'darwin'
|
||||
? DarwinTestRunner
|
||||
: PosixTestRunner;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../../extensions/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
"types": [
|
||||
"node",
|
||||
"mocha",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../../src/vscode-dts/vscode.d.ts",
|
||||
"../../../src/vscode-dts/vscode.proposed.testObserver.d.ts",
|
||||
"../../../src/vscode-dts/vscode.proposed.testRelatedCode.d.ts"
|
||||
]
|
||||
}
|
||||
Vendored
+686
@@ -0,0 +1,686 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Gulp Build",
|
||||
"program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js",
|
||||
"stopOnEntry": true,
|
||||
"args": [
|
||||
"hygiene"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"name": "Attach to Extension Host",
|
||||
"timeout": 0,
|
||||
"port": 5870,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js",
|
||||
"${workspaceFolder}/extensions/*/out/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"name": "Attach to Shared Process",
|
||||
"timeout": 0,
|
||||
"port": 5879,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Search Process",
|
||||
"port": 5876,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Pty Host Process",
|
||||
"port": 5877,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to CLI Process",
|
||||
"port": 5874,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Main Process",
|
||||
"timeout": 30000,
|
||||
"port": 5875,
|
||||
"continueOnAttach": true,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Emmet Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/emmet/test-workspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/emmet",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/emmet/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Configuration Editing Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/configuration-editing",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/configuration-editing/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Git Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"/tmp/my4g9l",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/git",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/git/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/git/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Github Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/github/testWorkspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/github",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/github/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/github/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code API Tests (single folder)",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
// "${workspaceFolder}", // Uncomment for running out of sources.
|
||||
"${workspaceFolder}/extensions/vscode-api-tests/testWorkspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/singlefolder-tests",
|
||||
"--disable-extensions"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/vscode-api-tests/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code API Tests (workspace)",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/vscode-api-tests/testworkspace.code-workspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/workspace-tests"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/vscode-api-tests/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Tokenizer Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/vscode-colorize-tests/test",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-colorize-tests",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/vscode-colorize-tests/out"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "VS Code Tokenizer Performance Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/vscode-colorize-perf-tests/test",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-colorize-perf-tests",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/vscode-colorize-perf-tests/out"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"name": "Attach to VS Code",
|
||||
"browserAttachLocation": "workspace",
|
||||
"port": 9222,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"perScriptSourcemaps": "yes"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch VS Code Internal",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.bat"
|
||||
},
|
||||
"osx": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.sh"
|
||||
},
|
||||
"linux": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.sh"
|
||||
},
|
||||
"port": 9222,
|
||||
"timeout": 0,
|
||||
"env": {
|
||||
"VSCODE_EXTHOST_WILL_SEND_SOCKET": null,
|
||||
"VSCODE_SKIP_PRELAUNCH": "1",
|
||||
},
|
||||
"cleanUp": "wholeBrowser",
|
||||
"runtimeArgs": [
|
||||
"--inspect-brk=5875",
|
||||
"--no-cached-data",
|
||||
"--crash-reporter-directory=${workspaceFolder}/.profile-oss/crashes",
|
||||
// for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910
|
||||
"--disable-features=CalculateNativeWinOcclusion",
|
||||
"--disable-extension=vscode.vscode-api-tests"
|
||||
],
|
||||
"userDataDir": "${userHome}/.vscode-oss-dev",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"cascadeTerminateToConfigurations": [
|
||||
"Attach to Extension Host"
|
||||
],
|
||||
"pauseForSourceMap": false,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"browserLaunchLocation": "workspace",
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// To debug observables you also need the extension "ms-vscode.debug-value-editor"
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch VS Code Internal (Dev Debug)",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.bat"
|
||||
},
|
||||
"osx": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.sh"
|
||||
},
|
||||
"linux": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.sh"
|
||||
},
|
||||
"port": 9222,
|
||||
"timeout": 0,
|
||||
"env": {
|
||||
"VSCODE_EXTHOST_WILL_SEND_SOCKET": null,
|
||||
"VSCODE_SKIP_PRELAUNCH": "1",
|
||||
"VSCODE_DEV_DEBUG": "1",
|
||||
},
|
||||
"cleanUp": "wholeBrowser",
|
||||
"runtimeArgs": [
|
||||
"--inspect-brk=5875",
|
||||
"--no-cached-data",
|
||||
"--crash-reporter-directory=${workspaceFolder}/.profile-oss/crashes",
|
||||
// for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910
|
||||
"--disable-features=CalculateNativeWinOcclusion",
|
||||
"--disable-extension=vscode.vscode-api-tests"
|
||||
],
|
||||
"userDataDir": "${userHome}/.vscode-oss-dev",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"cascadeTerminateToConfigurations": [
|
||||
"Attach to Extension Host"
|
||||
],
|
||||
"pauseForSourceMap": false,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"browserLaunchLocation": "workspace",
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "VS Code Server (Web)",
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code-server.sh",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code-server.bat",
|
||||
},
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Main Process",
|
||||
"attachSimplePort": 5875,
|
||||
"enableContentValidation": false,
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.sh",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.bat",
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--inspect-brk=5875",
|
||||
"--no-cached-data",
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1_vscode",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"outFiles": [],
|
||||
"perScriptSourcemaps": "yes",
|
||||
"name": "VS Code Server (Web, Chrome)",
|
||||
"url": "http://localhost:8080?tkn=dev-token",
|
||||
"preLaunchTask": "Run code server",
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
"outFiles": [],
|
||||
"perScriptSourcemaps": "yes",
|
||||
"name": "VS Code Server (Web, Edge)",
|
||||
"url": "http://localhost:8080?tkn=dev-token",
|
||||
"pauseForSourceMap": false,
|
||||
"preLaunchTask": "Run code server",
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"outFiles": [],
|
||||
"perScriptSourcemaps": "yes",
|
||||
"name": "VS Code Web (Chrome)",
|
||||
"url": "http://localhost:8080",
|
||||
"preLaunchTask": "Run code web",
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
"outFiles": [],
|
||||
"perScriptSourcemaps": "yes",
|
||||
"name": "VS Code Web (Edge)",
|
||||
"url": "http://localhost:8080",
|
||||
"pauseForSourceMap": false,
|
||||
"preLaunchTask": "Run code web",
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Git Unit Tests",
|
||||
"program": "${workspaceFolder}/extensions/git/node_modules/mocha/bin/_mocha",
|
||||
"stopOnEntry": false,
|
||||
"cwd": "${workspaceFolder}/extensions/git",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/git/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "HTML Server Unit Tests",
|
||||
"program": "${workspaceFolder}/extensions/html-language-features/server/test/index.js",
|
||||
"stopOnEntry": false,
|
||||
"cwd": "${workspaceFolder}/extensions/html-language-features/server",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/html-language-features/server/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "CSS Server Unit Tests",
|
||||
"program": "${workspaceFolder}/extensions/css-language-features/server/test/index.js",
|
||||
"stopOnEntry": false,
|
||||
"cwd": "${workspaceFolder}/extensions/css-language-features/server",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/css-language-features/server/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "Markdown Extension Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/markdown-language-features/test-workspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/markdown-language-features",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/markdown-language-features/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/markdown-language-features/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "TypeScript Extension Tests",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"${workspaceFolder}/extensions/typescript-language-features/test-workspace",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/extensions/typescript-language-features",
|
||||
"--extensionTestsPath=${workspaceFolder}/extensions/typescript-language-features/out/test"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/extensions/typescript-language-features/out/**/*.js"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "5_tests",
|
||||
"order": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Unit Tests",
|
||||
"program": "${workspaceFolder}/test/unit/electron/index.js",
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.exe"
|
||||
},
|
||||
"linux": {
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/code-oss"
|
||||
},
|
||||
"outputCapture": "std",
|
||||
"args": [
|
||||
"--remote-debugging-port=9222"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"cascadeTerminateToConfigurations": [
|
||||
"Attach to VS Code"
|
||||
],
|
||||
"env": {
|
||||
"MOCHA_COLORS": "true"
|
||||
},
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Unit Tests For Current File",
|
||||
"program": "${workspaceFolder}/test/unit/electron/index.js",
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/Code - OSS.exe"
|
||||
},
|
||||
"linux": {
|
||||
"runtimeExecutable": "${workspaceFolder}/.build/electron/code-oss"
|
||||
},
|
||||
"cascadeTerminateToConfigurations": [
|
||||
"Attach to VS Code"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"args": [
|
||||
"--remote-debugging-port=9222",
|
||||
"--run",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"env": {
|
||||
"MOCHA_COLORS": "true"
|
||||
},
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Smoke Test",
|
||||
"program": "${workspaceFolder}/test/smoke/test/index.js",
|
||||
"cwd": "${workspaceFolder}/test/smoke",
|
||||
"timeout": 240000,
|
||||
"args": [
|
||||
"-l",
|
||||
"${workspaceFolder}/.build/electron/Code - OSS.app/Contents/MacOS/Electron"
|
||||
],
|
||||
"outFiles": [
|
||||
"${cwd}/out/**/*.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"VSCODE_DEV": "1",
|
||||
"VSCODE_CLI": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Built-in Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceRoot}/extensions/debug-auto-launch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Monaco Editor Playground",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:5001",
|
||||
"preLaunchTask": "Launch Http Server",
|
||||
"presentation": {
|
||||
"group": "monaco",
|
||||
"order": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "VS Code",
|
||||
"stopAll": true,
|
||||
"configurations": [
|
||||
"Launch VS Code Internal",
|
||||
"Attach to Main Process",
|
||||
"Attach to Extension Host",
|
||||
"Attach to Shared Process",
|
||||
],
|
||||
"preLaunchTask": "Ensure Prelaunch Dependencies",
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "VS Code (Debug Observables)",
|
||||
"stopAll": true,
|
||||
"configurations": [
|
||||
"Launch VS Code Internal (Dev Debug)",
|
||||
"Attach to Main Process",
|
||||
"Attach to Extension Host",
|
||||
"Attach to Shared Process",
|
||||
],
|
||||
"preLaunchTask": "Ensure Prelaunch Dependencies",
|
||||
"presentation": {
|
||||
"group": "0_vscode",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Search, Renderer, and Main processes",
|
||||
"configurations": [
|
||||
"Launch VS Code Internal",
|
||||
"Attach to Main Process",
|
||||
"Attach to Search Process"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1_vscode",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Renderer, Extension Host, and Main processes",
|
||||
"configurations": [
|
||||
"Launch VS Code Internal",
|
||||
"Attach to Main Process",
|
||||
"Attach to Extension Host"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1_vscode",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Unit Tests",
|
||||
"configurations": [
|
||||
"Attach to VS Code",
|
||||
"Run Unit Tests"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1_vscode",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Unit Tests (Current File)",
|
||||
"configurations": [
|
||||
"Attach to VS Code",
|
||||
"Run Unit Tests For Current File"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1_vscode",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Renderer and Main processes",
|
||||
"stopAll": true,
|
||||
"configurations": [
|
||||
"Launch VS Code Internal",
|
||||
"Attach to Main Process"
|
||||
],
|
||||
"preLaunchTask": "Ensure Prelaunch Dependencies"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "#### Config"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"March 2025\""
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "### Finalization"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPO $MILESTONE label:api-finalization"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "### Proposals"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPO $MILESTONE is:open label:api-proposal sort:created-asc"
|
||||
}
|
||||
]
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
[
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "#### Macros"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"March 2025\""
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# Preparation"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Open Pull Requests on the Milestone"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:pr is:open"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Unverified Older Insiders-Released Issues"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:bug label:insiders-released -label:verified -label:*duplicate -label:*as-designed -label:z-author-verified -label:on-testplan -label:error-telemetry"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Unverified Older Insiders-Released Feature Requests"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS -$MILESTONE is:issue is:closed reason:completed label:feature-request label:insiders-released -label:on-testplan -label:verified -label:*duplicate -label:error-telemetry"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Open Issues on the Milestone"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:open -label:iteration-plan -label:endgame-plan -label:testplan-item"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Feature Requests Missing Labels"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:closed reason:completed label:feature-request -label:verification-needed -label:on-testplan -label:verified -label:*duplicate"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Open Test Plan Items without milestone"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:open label:testplan-item no:milestone"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# Testing"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Test Plan Items"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS is:issue is:open label:testplan-item"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Verification Needed"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:closed reason:completed label:verification-needed -label:verified -label:on-testplan"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# Verification"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Verifiable Fixes"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified -label:unreleased -label:*not-reproducible -label:*out-of-scope"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Verifiable Fixes Missing Steps"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug label:verification-steps-needed -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified -label:unreleased -label:*not-reproducible"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Unreleased Fixes"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:z-author-verified label:unreleased -label:*not-reproducible"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## All Unverified Fixes"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:closed reason:completed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:z-author-verified -label:*not-reproducible"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# Candidates"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$REPOS $MILESTONE is:issue is:open label:candidate"
|
||||
}
|
||||
]
|
||||
+667
@@ -0,0 +1,667 @@
|
||||
[
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Config"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$since=2021-10-01"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode\n\nQuery exceeds the maximum result. Run the query manually: `is:issue is:open closed:>2021-10-01`"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "//repo:microsoft/vscode is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "//repo:microsoft/vscode is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-remote-release"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-remote-release is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-remote-release is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-editor"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-editor is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-editor is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-docs"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-docs is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-docs is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-js-debug"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-js-debug is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-js-debug is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# language-server-protocol"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/language-server-protocol is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/language-server-protocol is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-eslint"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-eslint is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-eslint is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-css-languageservice"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-css-languageservice is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-css-languageservice is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-test"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-test is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-test is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-pull-request-github"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-pull-request-github is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-test is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-chrome-debug-core"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-chrome-debug-core is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-chrome-debug-core is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-debugadapter-node"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-debugadapter-node is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-debugadapter-node is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-emmet-helper"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-emmet-helper is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-emmet-helper is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-extension-vscode\n\nDeprecated"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-extension-vscode is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-extension-vscode is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-extension-samples"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-extension-samples is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-extension-samples is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-filewatcher-windows"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-filewatcher-windows is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-filewatcher-windows is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-generator-code"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-generator-code is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-generator-code is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-html-languageservice"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-html-languageservice is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-html-languageservice is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-json-languageservice"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-json-languageservice is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-json-languageservice is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-languageserver-node"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-languageserver-node is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-languageserver-node is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-loader"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-loader is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-loader is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-mono-debug"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-mono-debug is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-mono-debug is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-node-debug"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-node-debug is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-node-debug is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-node-debug2"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-node-debug2 is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-node-debug2 is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-recipes"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-recipes is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-recipes is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-textmate"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-textmate is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-textmate is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-themes"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-themes is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-themes is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-vsce"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-vsce is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-vsce is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-website"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-website is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-website is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-windows-process-tree"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-windows-process-tree is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-windows-process-tree is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# debug-adapter-protocol"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/debug-adapter-protocol is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/debug-adapter-protocol is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# inno-updater"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/inno-updater is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/inno-updater is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-languages"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-languages is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-languages is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-typescript"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-typescript is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-typescript is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-css"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-css is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-css is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-json"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-json is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-json is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-html"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-html is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-html is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# monaco-editor-webpack-plugin"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-editor-webpack-plugin is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/monaco-editor-webpack-plugin is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# node-jsonc-parser"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/node-jsonc-parser is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/node-jsonc-parser is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-jupyter"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-jupyter is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-jupyter is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-python"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-python is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-python is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-livepreview"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-livepreview is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-livepreview is:issue created:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "# vscode-test"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-test is:issue closed:>$since"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode-test is:issue created:>$since"
|
||||
}
|
||||
]
|
||||
+52
File diff suppressed because one or more lines are too long
+47
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## tl;dr: Triage Inbox\n\nAll inbox issues but not those that need more information. These issues need to be triaged, e.g assigned to a user or ask for more information"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$inbox -label:\"info-needed\" sort:created-desc"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "repo:microsoft/vscode label:triage-needed is:open"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "##### `Config`: defines the inbox query"
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$inbox=repo:microsoft/vscode is:open no:assignee -label:feature-request -label:testplan-item -label:plan-item "
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## Inbox tracking and Issue triage"
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "New issues or pull requests submitted by the community are initially triaged by an [automatic classification bot](https://github.com/microsoft/vscode-github-triage-actions/tree/master/classifier-deep). Issues that the bot does not correctly triage are then triaged by a team member. The team rotates the inbox tracker on a weekly basis.\n\nA [mirror](https://github.com/JacksonKearl/testissues/issues) of the VS Code issue stream is available with details about how the bot classifies issues, including feature-area classifications and confidence ratings. Per-category confidence thresholds and feature-area ownership data is maintained in [.github/classifier.json](https://github.com/microsoft/vscode/blob/main/.github/classifier.json). \n\n💡 The bot is being run through a GitHub action that runs every 30 minutes. Give the bot the opportunity to classify an issue before doing it manually.\n\n### Inbox Tracking\n\nThe inbox tracker is responsible for the [global inbox](https://github.com/microsoft/vscode/issues?utf8=%E2%9C%93&q=is%3Aopen+no%3Aassignee+-label%3Afeature-request+-label%3Atestplan-item+-label%3Aplan-item) containing all **open issues and pull requests** that\n- are neither **feature requests** nor **test plan items** nor **plan items** and\n- have **no owner assignment**.\n\nThe **inbox tracker** may perform any step described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) but its main responsibility is to route issues to the actual feature area owner.\n\nFeature area owners track the **feature area inbox** containing all **open issues and pull requests** that\n- are personally assigned to them and are not assigned to any milestone\n- are labeled with their feature area label and are not assigned to any milestone.\nThis secondary triage may involve any of the steps described in our [issue triaging documentation](https://github.com/microsoft/vscode/wiki/Issues-Triaging) and results in a fully triaged or closed issue.\n\nThe [github triage extension](https://github.com/microsoft/vscode-github-triage-extension) can be used to assist with triaging — it provides a \"Command Palette\"-style list of triaging actions like assignment, labeling, and triggers for various bot actions."
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"language": "markdown",
|
||||
"value": "## All Inbox Items\n\nAll issues that have no assignee and that have neither **feature requests** nor **test plan items** nor **plan items**."
|
||||
},
|
||||
{
|
||||
"kind": 2,
|
||||
"language": "github-issues",
|
||||
"value": "$inbox"
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user