Skip to content

Handle namespace suggestions for external packages #1689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ Since 2018.1, the version numbers and release cycle match Rider's versions and r

- Add Unity code cleanup patterns that do not reorder serialised fields ([#88](https://github.com/JetBrains/resharper-unity/issues/88), [#1676](https://github.com/JetBrains/resharper-unity/pull/1676))
- Use `Range` attribute to provide hints to integer dataflow analysis ([#1673](https://github.com/JetBrains/resharper-unity/pull/1673))
- Improve namespace suggestions for packages ([#1161](https://github.com/JetBrains/resharper-unity/issues/1161), [RIDER-36546](https://youtrack.jetbrains.com/issue/RIDER-36546), [#1677](https://github.com/JetBrains/resharper-unity/pull/1677))
- Improve namespace suggestions for packages ([#1161](https://github.com/JetBrains/resharper-unity/issues/1161), [RIDER-36546](https://youtrack.jetbrains.com/issue/RIDER-36546), [#1677](https://github.com/JetBrains/resharper-unity/pull/1677), [#1689](https://github.com/JetBrains/resharper-unity/pull/1689))
- Rider: Add "pausepoints" a type of breakpoint that doesn't suspend code execution, but pauses the Unity editor ([#1272](https://github.com/JetBrains/resharper-unity/issues/1272), [#1661](https://github.com/JetBrains/resharper-unity/pull/1661))
- Rider: Automatically use UnityYamlMerge to merge asset files ([RIDER-33411](https://youtrack.jetbrains.com/issue/RIDER-33411), [#1682](https://github.com/JetBrains/resharper-unity/pull/1682))
- Rider: Add sample text for "Unity", "ShaderLab" and "Cg/HLSL" Colour Scheme options pages ([#1667](https://github.com/JetBrains/resharper-unity/pull/1667))

### Changed
Expand All @@ -28,6 +29,8 @@ Since 2018.1, the version numbers and release cycle match Rider's versions and r
- Rider: Better support for prefab modifications in Find Usages and showing Inspector values ([#1645](https://github.com/JetBrains/resharper-unity/pull/1645))
- Rider: Show method handlers for Unity events in the editor ([#1645](https://github.com/JetBrains/resharper-unity/pull/1645))
- Rider: Disable "Start Unity" action when Unity is running ([RIDER-36108](https://youtrack.jetbrains.com/issue/RIDER-36108), [#1554](https://github.com/JetBrains/resharper-unity/pull/1554))
- Rider: Unity Log view optionally scrolls to show new items ([RIDER-14377](https://youtrack.jetbrains.com/issue/RIDER-14377), [#1678](https://github.com/JetBrains/resharper-unity/pull/1678))
- Rider: Play/pause/step buttons no longer disabled while Rider is indexing ([#1678](https://github.com/JetBrains/resharper-unity/pull/1678))

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using JetBrains.Annotations;
using JetBrains.Diagnostics;
using JetBrains.ProjectModel;
using JetBrains.ProjectModel.Properties;
using JetBrains.ProjectModel.Properties.Managed;
using JetBrains.ReSharper.Plugins.Unity.ProjectModel;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.Util;

namespace JetBrains.ReSharper.Plugins.Unity.CSharp.Psi.Util
{
[SolutionComponent]
public class ExternalPackageCustomNamespaceProvider : ICustomDefaultNamespaceProvider
{
private static readonly Key<string> ourCalculatedDefaultNamespaceKey = new Key<string>("Unity::ExternalPackage::DefaultNamespace");

public ExpectedNamespaceAndNamespaceChecker CalculateCustomNamespace(IProjectItem projectItem, PsiLanguageType language)
{
var project = projectItem.GetProject().NotNull();
if (!ShouldProvideCustomNamespace(project, projectItem, language))
return new ExpectedNamespaceAndNamespaceChecker(null);

var namespaceFolderProperty = project.GetComponent<NamespaceFolderProperty>();
return new ExpectedNamespaceAndNamespaceChecker(CalculateNamespace(projectItem, language.LanguageService(),
namespaceFolderProperty));
}

private static bool ShouldProvideCustomNamespace(IProject project, IProjectItem projectItem, PsiLanguageType language)
{
if (!project.IsUnityProject() || !language.Is<CSharpLanguage>())
return false;

// If the project item lives under the solution location, we don't need to do anything. The default
// namespace handling, coupled with our namespace provider settings in NamespaceProviderProjectSettingsProvider
// means everything works
if (project.Location.IsPrefixOf(projectItem.Location))
return false;

// If the project has a default namespace, do nothing. When the project is based on an .asmdef file, the
// linked root folder is the location of the .asmdef file, and the namespaces we want are relative to the
// .asmdef file's default namespace
var buildSettings = project.ProjectProperties.BuildSettings as IManagedProjectBuildSettings;
var defaultNamespace = buildSettings?.DefaultNamespace;
if (!defaultNamespace.IsNullOrEmpty())
return false;

// Is the root folder a linked folder, pointing externally to the solution?
var rootFolder = GetRootFolder(projectItem);
return rootFolder?.IsLinked == true;
}

[CanBeNull]
private static IProjectFolder GetRootFolder(IProjectItem projectItem)
{
var projectFolder = projectItem.ParentFolder;
while (projectFolder != null)
{
if (projectFolder.ParentFolder is IProject)
return projectFolder;

projectFolder = projectFolder.ParentFolder;
}

return null;
}

[CanBeNull]
private static string CalculateNamespace([NotNull] IProjectItem item, LanguageService languageService,
NamespaceFolderProperty namespaceFolderProperty)
{
if (item.ParentFolder is IProject && item is IProjectFolder rootFolder)
return CalculateNamespaceForProjectFromRootFolder(item.GetProject(), rootFolder, languageService);

var parentFolder = item.ParentFolder;
if (parentFolder == null) return null;

var parentNamespace = CalculateNamespace(parentFolder, languageService, namespaceFolderProperty);
if (parentNamespace == null)
return null;

if (item is IProjectFolder folder)
{
var isNamespaceProvider = namespaceFolderProperty.GetNamespaceFolderProperty(folder);
if (!isNamespaceProvider)
return parentNamespace;
}

if (item is IProjectFile)
return parentNamespace;

var suffix = NamespaceFolderUtil.MakeValidQualifiedName(item.Name, languageService);

if (parentNamespace.Length > 0)
{
if (string.IsNullOrEmpty(suffix)) return parentNamespace;
return $"{parentNamespace}.{suffix}";
}

return suffix;
}

[CanBeNull]
private static string CalculateNamespaceForProjectFromRootFolder(
[CanBeNull] IProject project, IProjectFolder rootFolder, LanguageService languageService)
{
if (project == null || !rootFolder.IsLinked || rootFolder.Path == null) return null;

var calculatedNamespace = project.GetData(ourCalculatedDefaultNamespaceKey);
if (calculatedNamespace != null)
return calculatedNamespace;

var location = rootFolder.Path.ReferencedFolderPath;
var packageJsonDirectory = FileSystemUtil.TryGetDirectoryNameOfFileAbove(location, "package.json");
if (packageJsonDirectory != null)
{
var path = location.MakeRelativeTo(packageJsonDirectory);

// MAKE SURE TO KEEP UP TO DATE WITH THE RULES IN NamespaceProviderProjectSettingsProvider
if (path.StartsWith("Runtime/Scripts"))
path = path.RemovePrefix("Runtime/Scripts");
else if (path.StartsWith("Scripts/Runtime"))
path = path.RemovePrefix("Scripts/Runtime");
else if (path.StartsWith("Runtime"))
path = path.RemovePrefix("Runtime");
else if (path.StartsWith("Scripts"))
path = path.RemovePrefix("Scripts");

foreach (var pathComponent in path.Components)
{
var name = NamespaceFolderUtil.MakeValidQualifiedName(pathComponent.ToString(), languageService);
calculatedNamespace = calculatedNamespace.IsNullOrEmpty() ? name : $"{calculatedNamespace}.{name}";
}
}

project.PutData(ourCalculatedDefaultNamespaceKey, calculatedNamespace);

return calculatedNamespace;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,46 @@ public NamespaceProviderProjectSettingsProvider(ISettingsSchema settingsSchema,
myLogger = logger;
}

// The reasoning behind this is fairly simple:
// * Package location should not affect namespace
// The package owner is not in control of the location of the package on an end user's system. It might be
// cloned into the Packages folder, or referenced and cached in the Library/PackageCache folder. It might also
// be referenced in an external location with the file: syntax
// * Assembly definition location should affect location, but only relative to Assets or package root folder
// Packages are self contained units. The location of an assembly definition in a folder structure (especially
// the Assets folder) is deliberate on the part of the user and should be part of the namespace
// * If the asmdef defines a root namespace (Unity 2020.2a12+) this overrides everything, up to and including
// the location of the .asmdef file
// * Exclude some folder names, based on observed convention. E.g. Assets, Runtime, Scripts.
// Editor is deliberately not excluded.
//
// There are subtleties:
// * Exclude Assets and Assets/Scripts, Packages and Library/PackageCache
// * Exclude the package root folder. The package owner is not in control of this.
// Referenced packages are stored in Library/PackageCache with a folder name based on ID and version
// End users can clone git repos or tarballs into any folder
// * Exclude any folder until we get to the package root (the location of the package.json)
// If we clone a git repo into Packages, and the repo doesn't have a package in the root of the repo, skip all
// folders until we get to the package root. The package can be included via a file: entry in manifest.json
// * A package that lives inside the solution structure, but outside of Assets or Packages will have a project
// with a path that is relative to the solution root. Ignore all intermediary folders between solution and
// package root
// * A package that lives outside of the solution structure will have a root folder that is a link to the common
// parent of all files in the project. This is usually the asmdef location, AND IS INCORRECT.
// The linked folder is not included in namespace suggestions
// TODO: The linked folder should be the package root folder (which we normally ignore)
// This requires changes to the generated files. Either we include package.json to make a new default folder,
// or we add Link attributes to each file item
// * After package root, ignore Runtime, Scripts and any combination of the two
//
// And implications:
// * If there is no root namespace specified for an assembly definition in a package, one can be inferred from
// the path from the package root to the assembly definition file
// E.g. /Packages/[email protected]/Unity.Collections/Unity.Collections.asmdef will have
// a "root namespace" of Unity.Collections
public void InitialiseProjectSettings(Lifetime projectLifetime, IProject project,
ISettingsStorageMountPoint mountPoint)
{
// Don't require Assets or Assets.Scripts in namespaces
ExcludeFolderFromNamespace(mountPoint, "Assets");
ExcludeFolderFromNamespace(mountPoint, @"Assets\Scripts");

Expand All @@ -36,6 +72,8 @@ public void InitialiseProjectSettings(Lifetime projectLifetime, IProject project
ExcludeFolderFromNamespace(mountPoint, "Library");
ExcludePackagesFoldersFromNamespace(mountPoint, projectFolder, "PackageCache", @"Library");
}

ExcludeExternalPackagesFromNamespace(mountPoint, project);
}

private void ExcludeFolderFromNamespace(ISettingsStorageMountPoint mountPoint, string path)
Expand Down Expand Up @@ -65,29 +103,56 @@ private void ExcludePackagesFoldersFromNamespace(ISettingsStorageMountPoint moun
private void ExcludePackageSubFoldersFromNamespace(ISettingsStorageMountPoint mountPoint,
IProjectFolder thisFolder, string thisPath)
{
// First time called, this excludes package name, e.g. `Packages/com.unity.mathematics` or
// `Library/PackageCache/[email protected]`
// If that folder does not have a package.json recurse until we find one, and exclude everything along the
// way. This handles an edge case where e.g. a GitHub repo is checked out into Packages, but the root of the
// project isn't the package. Instead, the package is defined one or two levels down, and included into the
// project via file: in the manifest.
// E.g. myrepo/src/mypackage/{package.json}. We don't want `myrepo` or `src` appearing in the namespace
foreach (var subFolder in thisFolder.GetSubFolders())
{
var path = thisPath + @"\" + subFolder.Name;
ExcludeFolderFromNamespace(mountPoint, path);
ExcludePackageRootFolderFromNamespace(mountPoint, subFolder, path);
}
}

private void ExcludePackageRootFolderFromNamespace(ISettingsStorageMountPoint mountPoint,
IProjectFolder folder, string path)
{
ExcludeFolderFromNamespace(mountPoint, path);

// Is it a package folder? Exclude Scripts, Runtime, Scripts/Runtime and Runtime/Scripts
if (folder.Location.Combine("package.json").ExistsFile)
{
// The folder will be linked if the project's files belong to a package that is external to the solution
// folder. With a linked folder, the namespace provider must be the actual path, relative to the parent
// folder, rather than the visible path in the Solution Explorer
//
// NOTE: KEEP UP TO DATE WITH ExternalPackageCustomNamespaceProvider!
if (folder.IsLinked && folder.ParentFolder != null)
path = folder.Location.ConvertToRelativePath(folder.ParentFolder.Location).FullPath;
ExcludeFolderFromNamespace(mountPoint, path + @"\Runtime");
ExcludeFolderFromNamespace(mountPoint, path + @"\Scripts");
ExcludeFolderFromNamespace(mountPoint, path + @"\Runtime\Scripts");
ExcludeFolderFromNamespace(mountPoint, path + @"\Scripts\Runtime");
return;
}

// Recurse until we hit the package root
ExcludePackageSubFoldersFromNamespace(mountPoint, folder, path);
}

// Is it a package folder? Exclude Scripts, Runtime, Scripts/Runtime and Runtime/Scripts
if (subFolder.Location.Combine("package.json").ExistsFile)
private void ExcludeExternalPackagesFromNamespace(ISettingsStorageMountPoint mountPoint, IProject project)
{
// Handle assembly definitions for packages that live outside of the Unity project structure (i.e. not under
// Assets, Packages or Library/PackageCache), or that lives outside of the solution completely.
// If the assembly definition lives outside of the solution, this folder will be a link to the assembly
// definition location, NOT the package root. If it lives in the Unity solution folder, it will be the start
// of the path to the project root
foreach (var projectFolder in project.GetSubFolders())
{
if (projectFolder.Name.Equals("Assets", StringComparison.OrdinalIgnoreCase)
|| projectFolder.Name.Equals("Library", StringComparison.OrdinalIgnoreCase)
|| projectFolder.Name.Equals("Packages", StringComparison.OrdinalIgnoreCase))
{
ExcludeFolderFromNamespace(mountPoint, path + @"\Runtime");
ExcludeFolderFromNamespace(mountPoint, path + @"\Scripts");
ExcludeFolderFromNamespace(mountPoint, path + @"\Runtime\Scripts");
ExcludeFolderFromNamespace(mountPoint, path + @"\Scripts\Runtime");
return;
continue;
}

ExcludePackageSubFoldersFromNamespace(mountPoint, subFolder, path);
ExcludePackageRootFolderFromNamespace(mountPoint, projectFolder, projectFolder.Name);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Added:

- Add Unity code cleanup patterns that do not reorder serialised fields (#88, #1676)
- Use Range attribute to provide hints to integer dataflow analysis (#1673)
- Improve namespace suggestions for packages (#1161, RIDER-36546, #1677)
- Improve namespace suggestions for packages (#1161, RIDER-36546, #1677, #1689)

Changed:

Expand Down
Loading