1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat(lsp): quick fix for @deno-types="npm:@types/*" (#25954)

This commit is contained in:
Nayeem Rahman 2024-10-01 22:55:02 +01:00 committed by GitHub
parent f930000415
commit 3881b71734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 545 additions and 25 deletions

12
Cargo.lock generated
View file

@ -1492,9 +1492,9 @@ dependencies = [
[[package]]
name = "deno_doc"
version = "0.150.0"
version = "0.150.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c762829006b555837691b7016828eb1f93acf0a4ff344357b946898ea5b5610d"
checksum = "0841188bc852535b76e53be6c3d13c61cfc6751a731969b8959fe31fa696c73f"
dependencies = [
"ammonia",
"anyhow",
@ -1588,9 +1588,9 @@ dependencies = [
[[package]]
name = "deno_graph"
version = "0.82.3"
version = "0.83.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938ed2efa1dd9fdcceeebc169b2b7910506b8dacc992cfdcffd84aa6a3eb8db0"
checksum = "20088a4497b1a212482883dc7b0365e99f703d575fb512d4a793531cdc92ea76"
dependencies = [
"anyhow",
"async-trait",
@ -2832,9 +2832,9 @@ checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
[[package]]
name = "eszip"
version = "0.78.0"
version = "0.79.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0546f00d41dbc6e90b50e922759c02559a897e59b683369c3a13519cd5108b6"
checksum = "8eb55c89bdde75a3826a79d49c9d847623ae7fbdb2695b542982982da990d33e"
dependencies = [
"anyhow",
"async-trait",

View file

@ -67,8 +67,8 @@ deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "proposa
deno_cache_dir = { workspace = true }
deno_config = { version = "=0.35.0", features = ["workspace", "sync"] }
deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] }
deno_doc = { version = "0.150.0", features = ["html", "syntect"] }
deno_graph = { version = "=0.82.3" }
deno_doc = { version = "0.150.1", features = ["html", "syntect"] }
deno_graph = { version = "=0.83.0" }
deno_lint = { version = "=0.67.0", features = ["docs"] }
deno_lockfile.workspace = true
deno_npm.workspace = true
@ -79,7 +79,7 @@ deno_runtime = { workspace = true, features = ["include_js_files_for_snapshottin
deno_semver.workspace = true
deno_task_shell = "=0.17.0"
deno_terminal.workspace = true
eszip = "=0.78.0"
eszip = "=0.79.1"
libsui = "0.4.0"
napi_sym.workspace = true
node_resolver.workspace = true

View file

@ -2,6 +2,7 @@
use super::diagnostics::DenoDiagnostic;
use super::diagnostics::DiagnosticSource;
use super::documents::Document;
use super::documents::Documents;
use super::language_server;
use super::resolver::LspResolver;
@ -9,7 +10,9 @@ use super::tsc;
use super::urls::url_to_uri;
use crate::args::jsr_url;
use crate::lsp::search::PackageSearchApi;
use crate::tools::lint::CliLinter;
use deno_config::workspace::MappedResolution;
use deno_lint::diagnostic::LintDiagnosticRange;
use deno_ast::SourceRange;
@ -1151,6 +1154,162 @@ impl CodeActionCollection {
..Default::default()
}));
}
pub async fn add_source_actions(
&mut self,
document: &Document,
range: &lsp::Range,
language_server: &language_server::Inner,
) {
async fn deno_types_for_npm_action(
document: &Document,
range: &lsp::Range,
language_server: &language_server::Inner,
) -> Option<lsp::CodeAction> {
let (dep_key, dependency, _) =
document.get_maybe_dependency(&range.end)?;
if dependency.maybe_deno_types_specifier.is_some() {
return None;
}
if dependency.maybe_code.maybe_specifier().is_none()
&& dependency.maybe_type.maybe_specifier().is_none()
{
// We're using byonm and the package is not cached.
return None;
}
let position = deno_graph::Position::new(
range.end.line as usize,
range.end.character as usize,
);
let import_range = dependency.imports.iter().find_map(|i| {
if json!(i.kind) != json!("es") && json!(i.kind) != json!("tsType") {
return None;
}
if !i.specifier_range.includes(&position) {
return None;
}
i.full_range.as_ref()
})?;
let referrer = document.specifier();
let file_referrer = document.file_referrer();
let config_data = language_server
.config
.tree
.data_for_specifier(file_referrer?)?;
let workspace_resolver = config_data.resolver.clone();
let npm_ref = if let Ok(resolution) =
workspace_resolver.resolve(&dep_key, document.specifier())
{
let specifier = match resolution {
MappedResolution::Normal { specifier, .. }
| MappedResolution::ImportMap { specifier, .. } => specifier,
_ => {
return None;
}
};
NpmPackageReqReference::from_specifier(&specifier).ok()?
} else {
// Only resolve bare package.json deps for byonm.
if !config_data.byonm {
return None;
}
if !language_server
.resolver
.is_bare_package_json_dep(&dep_key, referrer)
{
return None;
}
NpmPackageReqReference::from_str(&format!("npm:{}", &dep_key)).ok()?
};
let package_name = &npm_ref.req().name;
if package_name.starts_with("@types/") {
return None;
}
let managed_npm_resolver = language_server
.resolver
.maybe_managed_npm_resolver(file_referrer);
if let Some(npm_resolver) = managed_npm_resolver {
if !npm_resolver.is_pkg_req_folder_cached(npm_ref.req()) {
return None;
}
}
if language_server
.resolver
.npm_to_file_url(&npm_ref, document.specifier(), file_referrer)
.is_some()
{
// The package import has types.
return None;
}
let types_package_name = format!("@types/{package_name}");
let types_package_version = language_server
.npm_search_api
.versions(&types_package_name)
.await
.ok()
.and_then(|versions| versions.first().cloned())?;
let types_specifier_text =
if let Some(npm_resolver) = managed_npm_resolver {
let mut specifier_text = if let Some(req) =
npm_resolver.top_package_req_for_name(&types_package_name)
{
format!("npm:{req}")
} else {
format!("npm:{}@^{}", &types_package_name, types_package_version)
};
let specifier = ModuleSpecifier::parse(&specifier_text).ok()?;
if let Some(file_referrer) = file_referrer {
if let Some(text) = language_server
.get_ts_response_import_mapper(file_referrer)
.check_specifier(&specifier, referrer)
{
specifier_text = text;
}
}
specifier_text
} else {
types_package_name.clone()
};
let uri = language_server
.url_map
.specifier_to_uri(referrer, file_referrer)
.ok()?;
let position = lsp::Position {
line: import_range.start.line as u32,
character: import_range.start.character as u32,
};
let new_text = format!(
"{}// @deno-types=\"{}\"\n",
if position.character == 0 { "" } else { "\n" },
&types_specifier_text
);
let text_edit = lsp::TextEdit {
range: lsp::Range {
start: position,
end: position,
},
new_text,
};
Some(lsp::CodeAction {
title: format!(
"Add @deno-types directive for \"{}\"",
&types_specifier_text
),
kind: Some(lsp::CodeActionKind::QUICKFIX),
diagnostics: None,
edit: Some(lsp::WorkspaceEdit {
changes: Some([(uri, vec![text_edit])].into_iter().collect()),
..Default::default()
}),
..Default::default()
})
}
if let Some(action) =
deno_types_for_npm_action(document, range, language_server).await
{
self.actions.push(CodeActionKind::Deno(action));
}
}
}
/// Prepend the whitespace characters found at the start of line_content to content.

View file

@ -1517,17 +1517,19 @@ fn diagnose_dependency(
let import_ranges: Vec<_> = dependency
.imports
.iter()
.map(|i| documents::to_lsp_range(&i.range))
.map(|i| documents::to_lsp_range(&i.specifier_range))
.collect();
// TODO(nayeemrmn): This is a crude way of detecting `@deno-types` which has
// a different specifier and therefore needs a separate call to
// `diagnose_resolution()`. It would be much cleaner if that were modelled as
// a separate dependency: https://github.com/denoland/deno_graph/issues/247.
let is_types_deno_types = !dependency.maybe_type.is_none()
&& !dependency
.imports
.iter()
.any(|i| dependency.maybe_type.includes(&i.range.start).is_some());
&& !dependency.imports.iter().any(|i| {
dependency
.maybe_type
.includes(&i.specifier_range.start)
.is_some()
});
diagnostics.extend(
diagnose_resolution(

View file

@ -207,11 +207,11 @@ pub struct Inner {
module_registry: ModuleRegistry,
/// A lazily create "server" for handling test run requests.
maybe_testing_server: Option<testing::TestServer>,
npm_search_api: CliNpmSearchApi,
pub npm_search_api: CliNpmSearchApi,
project_version: usize,
/// A collection of measurements which instrument that performance of the LSP.
performance: Arc<Performance>,
resolver: Arc<LspResolver>,
pub resolver: Arc<LspResolver>,
task_queue: LanguageServerTaskQueue,
/// A memoized version of fixable diagnostic codes retrieved from TypeScript.
ts_fixable_diagnostics: Vec<String>,
@ -1612,8 +1612,8 @@ impl Inner {
None => false,
})
.collect();
if !fixable_diagnostics.is_empty() {
let mut code_actions = CodeActionCollection::default();
if !fixable_diagnostics.is_empty() {
let file_diagnostics = self
.diagnostics_server
.get_ts_diagnostics(&specifier, asset_or_doc.document_lsp_version());
@ -1721,9 +1721,14 @@ impl Inner {
.add_cache_all_action(&specifier, no_cache_diagnostics.to_owned());
}
}
}
if let Some(document) = asset_or_doc.document() {
code_actions
.add_source_actions(document, &params.range, self)
.await;
}
code_actions.set_preferred_fixes();
all_actions.extend(code_actions.get_response());
}
// Refactor
let only = params

View file

@ -328,11 +328,11 @@ impl LspResolver {
) -> Option<(ModuleSpecifier, MediaType)> {
let resolver = self.get_scope_resolver(file_referrer);
let node_resolver = resolver.node_resolver.as_ref()?;
Some(NodeResolution::into_specifier_and_media_type(
Some(NodeResolution::into_specifier_and_media_type(Some(
node_resolver
.resolve_req_reference(req_ref, referrer, NodeResolutionMode::Types)
.ok(),
))
.ok()?,
)))
}
pub fn in_node_modules(&self, specifier: &ModuleSpecifier) -> bool {
@ -373,6 +373,26 @@ impl LspResolver {
Some(NodeResolution::into_specifier_and_media_type(Some(resolution)).1)
}
pub fn is_bare_package_json_dep(
&self,
specifier_text: &str,
referrer: &ModuleSpecifier,
) -> bool {
let resolver = self.get_scope_resolver(Some(referrer));
let Some(node_resolver) = resolver.node_resolver.as_ref() else {
return false;
};
node_resolver
.resolve_if_for_npm_pkg(
specifier_text,
referrer,
NodeResolutionMode::Types,
)
.ok()
.flatten()
.is_some()
}
pub fn get_closest_package_json(
&self,
referrer: &ModuleSpecifier,

View file

@ -428,6 +428,16 @@ impl ManagedCliNpmResolver {
self.resolution.snapshot()
}
pub fn top_package_req_for_name(&self, name: &str) -> Option<PackageReq> {
let package_reqs = self.resolution.package_reqs();
let mut entries = package_reqs
.iter()
.filter(|(_, nv)| nv.name == name)
.collect::<Vec<_>>();
entries.sort_by_key(|(_, nv)| &nv.version);
Some(entries.last()?.0.clone())
}
pub fn serialized_valid_snapshot_for_system(
&self,
system_info: &NpmSystemInfo,

View file

@ -5906,6 +5906,135 @@ fn lsp_code_actions_deno_cache_all() {
client.shutdown();
}
#[test]
fn lsp_code_actions_deno_types_for_npm() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.add_npm_env_vars()
.build();
let temp_dir = context.temp_dir();
temp_dir.write("deno.json", json!({}).to_string());
temp_dir.write(
"package.json",
json!({
"dependencies": {
"react": "^18.2.0",
"@types/react": "^18.3.10",
},
})
.to_string(),
);
temp_dir.create_dir_all("managed_node_modules");
temp_dir.write(
"managed_node_modules/deno.json",
json!({
"nodeModulesDir": false,
})
.to_string(),
);
context.run_npm("install");
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import \"react\";\n",
}
}));
let res = client.write_request(
"textDocument/codeAction",
json!({
"textDocument": {
"uri": temp_dir.url().join("file.ts").unwrap(),
},
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 7 },
},
"context": { "diagnostics": [], "only": ["quickfix"] },
}),
);
assert_eq!(
res,
json!([
{
"title": "Add @deno-types directive for \"@types/react\"",
"kind": "quickfix",
"edit": {
"changes": {
temp_dir.url().join("file.ts").unwrap(): [
{
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 0 },
},
"newText": "// @deno-types=\"@types/react\"\n",
},
],
},
},
},
]),
);
client.did_open(json!({
"textDocument": {
"uri": temp_dir.url().join("managed_node_modules/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "import \"npm:react\";\n",
}
}));
client.write_request(
"workspace/executeCommand",
json!({
"command": "deno.cache",
"arguments": [
[],
temp_dir.url().join("managed_node_modules/file.ts").unwrap(),
],
}),
);
let res = client.write_request(
"textDocument/codeAction",
json!({
"textDocument": {
"uri": temp_dir.url().join("managed_node_modules/file.ts").unwrap(),
},
"range": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 7 },
},
"context": { "diagnostics": [], "only": ["quickfix"] },
}),
);
assert_eq!(
res,
json!([
{
"title": "Add @deno-types directive for \"npm:@types/react@^18.3.10\"",
"kind": "quickfix",
"edit": {
"changes": {
temp_dir.url().join("managed_node_modules/file.ts").unwrap(): [
{
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 0 },
},
"newText": "// @deno-types=\"npm:@types/react@^18.3.10\"\n",
},
],
},
},
},
]),
);
client.shutdown();
}
#[test]
fn lsp_cache_on_save() {
let context = TestContextBuilder::new()

View file

@ -0,0 +1,48 @@
{
"name": "@types/prop-types",
"dist-tags": { "latest": "15.7.13" },
"versions": {
"15.7.13": {
"name": "@types/prop-types",
"version": "15.7.13",
"license": "MIT",
"_id": "@types/prop-types@15.7.13",
"dist": {
"shasum": "2af91918ee12d9d32914feb13f5326658461b451",
"tarball": "http://localhost:4260/@types/prop-types/prop-types-15.7.13.tgz",
"fileCount": 5,
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"signatures": [
{
"sig": "MEUCIQCz+S+fZxW5ahdAmLsMoCnFlj4QvsEH0h70sXmSVc1EDgIgBDq1TH20PzRBFVHHY3gRc7JeNsAOozAmrQqCy+v1syk=",
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"
}
],
"unpackedSize": 6965
},
"main": "",
"types": "index.d.ts",
"scripts": {},
"repository": {
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"type": "git",
"directory": "types/prop-types"
},
"description": "TypeScript definitions for prop-types",
"directories": {},
"dependencies": {},
"_hasShrinkwrap": false,
"typeScriptVersion": "4.8",
"typesPublisherContentHash": "463997a2d1b4bb7e18554d9d12146cc74169e8bda6a4b9008306c38797ae14d3"
}
},
"license": "MIT",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/prop-types",
"repository": {
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"type": "git",
"directory": "types/prop-types"
},
"description": "TypeScript definitions for prop-types",
"readmeFilename": ""
}

Binary file not shown.

View file

@ -0,0 +1,71 @@
{
"name": "@types/react",
"dist-tags": { "latest": "18.3.10" },
"versions": {
"18.3.10": {
"name": "@types/react",
"version": "18.3.10",
"license": "MIT",
"_id": "@types/react@18.3.10",
"dist": {
"shasum": "6edc26dc22ff8c9c226d3c7bf8357b013c842219",
"tarball": "http://localhost:4260/@types/react/react-18.3.10.tgz",
"fileCount": 17,
"integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==",
"signatures": [
{
"sig": "MEUCIF6O9Y11lCvdqbD+e81mjGbAGN3pZa2cxxwi5Z+3bQr6AiEA0tDSc8Y9i7TjXbsZ2SnO/taFI6v99xnkZRDv5m6to7k=",
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"
}
],
"unpackedSize": 438614
},
"main": "",
"types": "index.d.ts",
"exports": {
".": {
"types": { "default": "./index.d.ts" },
"types@<=5.0": { "default": "./ts5.0/index.d.ts" }
},
"./canary": {
"types": { "default": "./canary.d.ts" },
"types@<=5.0": { "default": "./ts5.0/canary.d.ts" }
},
"./jsx-runtime": {
"types": { "default": "./jsx-runtime.d.ts" },
"types@<=5.0": { "default": "./ts5.0/jsx-runtime.d.ts" }
},
"./experimental": {
"types": { "default": "./experimental.d.ts" },
"types@<=5.0": { "default": "./ts5.0/experimental.d.ts" }
},
"./package.json": "./package.json",
"./jsx-dev-runtime": {
"types": { "default": "./jsx-dev-runtime.d.ts" },
"types@<=5.0": { "default": "./ts5.0/jsx-dev-runtime.d.ts" }
}
},
"scripts": {},
"repository": {
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"type": "git",
"directory": "types/react"
},
"description": "TypeScript definitions for react",
"directories": {},
"dependencies": { "csstype": "^3.0.2", "@types/prop-types": "*" },
"_hasShrinkwrap": false,
"typeScriptVersion": "4.8",
"typesPublisherContentHash": "3fbf914f5052668104237c78a8f67cca37176346a3caed94eea6d5c504795f08"
}
},
"license": "MIT",
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react",
"repository": {
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"type": "git",
"directory": "types/react"
},
"description": "TypeScript definitions for react",
"readmeFilename": ""
}

Binary file not shown.

View file

@ -2,7 +2,7 @@
"name": "csstype",
"description": "Strict TypeScript and Flow types for style based on MDN data",
"dist-tags": {
"latest": "2.6.20",
"latest": "3.1.3",
"version-2": "2.6.20"
},
"versions": {
@ -73,6 +73,82 @@
},
"directories": {},
"_hasShrinkwrap": false
},
"3.1.3": {
"name": "csstype",
"version": "3.1.3",
"main": "",
"types": "index.d.ts",
"description": "Strict TypeScript and Flow types for style based on MDN data",
"repository": {
"type": "git",
"url": "git+https://github.com/frenic/csstype.git"
},
"author": { "name": "Fredrik Nicol", "email": "fredrik.nicol@gmail.com" },
"license": "MIT",
"devDependencies": {
"@types/chokidar": "^2.1.3",
"@types/css-tree": "^2.3.1",
"@types/jest": "^29.5.0",
"@types/jsdom": "^21.1.1",
"@types/node": "^16.18.23",
"@types/prettier": "^2.7.2",
"@types/request": "^2.48.8",
"@types/turndown": "^5.0.1",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"eslint": "^8.37.0",
"css-tree": "^2.3.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"fast-glob": "^3.2.12",
"flow-bin": "^0.203.1",
"jest": "^29.5.0",
"jsdom": "^21.1.1",
"mdn-browser-compat-data": "git+https://github.com/mdn/browser-compat-data.git#1bf44517bd08de735e9ec20dbfe8e86c96341054",
"mdn-data": "git+https://github.com/mdn/data.git#7f0c865a3c4b5d891285c93308ee5c25cb5cfee8",
"prettier": "^2.8.7",
"request": "^2.88.2",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"turndown": "^7.1.2",
"typescript": "~5.0.3"
},
"scripts": {
"prepublish": "npm install --prefix __tests__ && npm install --prefix __tests__/__fixtures__",
"prepublishOnly": "tsc && npm run test:src && npm run build && ts-node --files prepublish.ts",
"update": "ts-node --files update.ts",
"build": "ts-node --files build.ts --start",
"watch": "ts-node --files build.ts --watch",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"pretty": "prettier --write build.ts **/*.{ts,js,json,md}",
"lazy": "tsc && npm run lint",
"test": "jest --runInBand",
"test:src": "jest src.*.ts",
"test:dist": "jest dist.*.ts --runInBand"
},
"gitHead": "fb448e21733ac5cb52523d3b678fdbbe1f9b42f2",
"bugs": { "url": "https://github.com/frenic/csstype/issues" },
"_id": "csstype@3.1.3",
"_nodeVersion": "18.16.0",
"_npmVersion": "9.5.1",
"dist": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"shasum": "d80ff294d114fb0e6ac500fbf85b60137d7eff81",
"tarball": "http://localhost:4260/csstype/csstype-3.1.3.tgz",
"fileCount": 5,
"unpackedSize": 1246074,
"signatures": [
{
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
"sig": "MEQCIF2zTnkc6R7cr7euidncjKp9gnQSpUmoKPzB/pvL4rsQAiB4E1mDQEtpvu9cjct0kaUbtTAQc+rBLAO65gAk+ykfig=="
}
]
},
"directories": {},
"_hasShrinkwrap": false
}
},
"author": {

View file

@ -1,4 +1,4 @@
[WILDCARD]Caching module info for http://127.0.0.1:4250/@denotest/module-graph/1.4.0/mod.ts
[WILDCARD]Caching module info for http://127.0.0.1:4250/@denotest/module-graph/1.4.0/other.ts
[WILDCARD]
Test { other: Other {} }
[WILDCARD]

View file

@ -1,4 +1,4 @@
[WILDCARD]Caching module info for http://127.0.0.1:4250/@denotest/module-graph2/1.4.0/mod.ts
[WILDCARD]Caching module info for http://127.0.0.1:4250/@denotest/module-graph2/1.4.0/other.ts
[WILDCARD]
Test { other: Other {} }
[WILDCARD]