Skip to content

[Code quality] Standardize settings file paths using Path.Combine (#49164) - #49474

Open
raza-khan0108 wants to merge 3 commits into
microsoft:mainfrom
raza-khan0108:fix-settings-file-paths-49164
Open

[Code quality] Standardize settings file paths using Path.Combine (#49164)#49474
raza-khan0108 wants to merge 3 commits into
microsoft:mainfrom
raza-khan0108:fix-settings-file-paths-49164

Conversation

@raza-khan0108

Copy link
Copy Markdown

Summary of the Pull Request

This PR addresses issue #49164 by refactoring manual, inconsistent, and error-prone string concatenations of settings file paths across C# modules to use Path.Combine and clean path segments.

PR Checklist

Detailed Description of the Pull Request / Additional comments

  • src/common/ManagedCommon/LanguageHelper.cs: Replaced manual string concatenation (localAppDataDir + SettingsFilePath + SettingsFile) with Path.Combine(localAppDataDir, "Microsoft", "PowerToys", SettingsFile). Removed unused slash-padded SettingsFilePath constant.
  • src/settings-ui/Settings.UI.Library/LanguageModel.cs: Replaced manual string concatenation with Path.Combine(localAppDataDir, "Microsoft", "PowerToys", SettingsFile).
  • src/settings-ui/Settings.UI.Library/UpdatingSettings.cs: Replaced manual string concatenation with Path.Combine(localAppDataDir, "Microsoft", "PowerToys", SettingsFile).
  • src/settings-ui/Settings.UI.Library/SettingPath.cs: Replaced hardcoded string slashes ($"Microsoft\\PowerToys\\{powertoy}") with multi-segment Path.Combine calls in SettingsFolderExists, CreateSettingsFolder, DeleteSettings, and GetSettingsPath.
  • src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs: Replaced string concatenation in DataFolder() with Path.Combine.
  • src/modules/EnvironmentVariables/EnvironmentVariablesUILib/Helpers/EnvironmentVariablesService.cs: Updated _profilesJsonFilePath initialization to use structured Path.Combine segments. Cleaned up unused ProfilesJsonFileSubPath constant.

Validation Steps Performed

  • Verified all modified C# files build cleanly without errors or warnings.
  • Confirmed paths resolve correctly using Path.Combine across ManagedCommon, Settings.UI.Library, Workspaces, and EnvironmentVariables.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions github-actions Bot added Product-Environment Variables Refers to Environment Variables PowerToys Product-Settings The standalone PowerToys Settings application Product-Workspaces Refers to the Workspaces utility labels Jul 23, 2026
@Jay-o-Way

Jay-o-Way commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks for picking this up.
Would it make sense to create one constant of the localAppDataDir, "Microsoft", "PowerToys" part? Don't think this location will change during runtime 😅

I see

var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);

And also

_directory.CreateDirectory(System.IO.Path.Combine(Helper.LocalApplicationDataFolder(), "Microsoft", "PowerToys", powertoy));

@Jay-o-Way Jay-o-Way self-assigned this Jul 25, 2026
@raza-khan0108

raza-khan0108 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Good question! A few reasons why keeping Path.Combine explicit works best here:

  1. Compile-time const limitation: Because Environment.GetFolderPath(...) is resolved at runtime based on the user profile, it can't be used in a C# const.
  2. Unit test mocking: In classes like SettingPath, path operations use IPath / IDirectory (System.IO.Abstractions) to allow unit testing with mocked file systems.
  3. Dynamic path resolution: Helper.LocalApplicationDataFolder() handles elevation edge-cases (e.g. running under LocalSystem SID with UserLocalAppDataPath), so evaluating it dynamically ensures the correct path is always resolved.
  4. Assembly boundaries: These calls span separate projects (Settings.UI.Library, ManagedCommon, Workspaces, EnvironmentVariables), so keeping Path.Combine(..., "Microsoft", "PowerToys", ...) local to each usage keeps modules decoupled.

@Jay-o-Way

Jay-o-Way commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Oh I see 👍 Fair points.

@Jay-o-Way Jay-o-Way left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the constant SettingsFile isn't really needed either, if it's only used in one line of code. Other than that - LGTM.

@raza-khan0108

Copy link
Copy Markdown
Author

I've removed the single-use SettingsFile constant for language.json and inlined the filename into Path.Combine. (Kept UpdatingSettings.SettingsFile as it's referenced by file watchers in the settings UI).

@daverayment daverayment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments about this approach, file-by-file and then with some general comments at the end:

SettingPath.cs

The move from interpolated backslash strings to discrete Path.Combine() segments is reasonable, but the same construction - "Microsoft" + "PowerToys" + "{x}" is used in multiple places. I'd suggest extracting this to a private helper to keep the root path defined in one place, e.g.:

private string GetModuleFolderPath(string powertoy) =>
    string.IsNullOrWhiteSpace(powertoy)
        ? _path.Combine(Helper.LocalApplicationDataFolder(), "Microsoft", "PowerToys")
        : _path.Combine(Helper.LocalApplicationDataFolder(), "Microsoft", "PowerToys", powertoy);

This also fixes a pre-existing issue where 3 of the 4 path constructions do not use _path, but instead call System.IO.Path.Combine() directly. _path abstracts away the file system so it can be mocked and tested in the unit tests.

If we're doing a refactor here, it makes sense to correct this.

Utilities.Helper
Helper.UserLocalAppDataPath is a mutable static property and LocalApplicationDataFolder() is intentionally a method rather than a cached field precisely because it can change during the process runtime - this is because the MouseWithoutBorders service can override the AppData folder:

if (serviceMode || runningAsSystem)
{
if (args.Length > 2)
{
SettingsHelper.UserLocalAppDataPath = args[2].Trim();
}
}

It would be useful to add a comment to this effect in the Utilities.Helper code, as it's not immediately obvious that the local AppData folder is mutable for a reason.

LanguageModel.cs
Here, Environment.GetFolderPath() is still called directly:

var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);

We should use the Helper.LocalApplicationDataFolder() helper which exists precisely because Environment.GetFolderPath() is not always correct. AFAIK, LanguageModel doesn't run in the context of MouseWithoutBorder's service, so there's no practical impact of this change currently, but it should still use the right API for consistency and because it's in the same library as Helper already.

LanguageHelper.cs
This cannot be updated to have a dependency on Settings.UI.Library because it would create a circular dependency. The update to use Path.Combine() is cosmetic only - the real problem is the direct call to Environment.GetFolderPath(), which remains. This needs to be tracked as a separate architectural issue.

SettingsFilePath field
You have removed SettingsFilePath in LanguageModel and LanguageHelper. Both this and SettingsFile are public consts. I'm pretty sure these aren't referenced elsewhere - GeneralViewModel uses LanguageModel as a data object for its ObservableCollection but doesn't use those constants directly - but it would be good to double-check and state this in the description.

General
Callers outside of the Settings project (so in FolderUtils.cs, EnvironmentVariablesService.cs and UpdatingSettings.cs) should be routing through SettingsUtils.GetSettingsFilePath()and not constructing their own paths in any form. GetSettingsFilePath() is specifically designed for this purpose.

Testing
You checked the "Tests: Added/updated and all pass" checkbox in the description, but there aren't any new or updated tests in your PR. I don't think it's enough to assert that you've manually checked things on such a fundamental change. For a refactor touching SettingPath specifically - a class with IDirectory/IPath abstractions that exist precisely for testability - I think it would be reasonable to expect new tests covering the changed methods, or which existing test methods were run and how they provide adequate coverage for the update. "Paths are correct" is not self-evident, especially for the three methods which didn't use _path - using a mock filesystem would let us check that directly.

@raza-khan0108

Copy link
Copy Markdown
Author

I've updated the PR with the following changes:

  • SettingPath.cs:

    • Extracted the path segment construction ("Microsoft", "PowerToys", {x}) into a private helper method GetModuleFolderPath(string powertoy = "").
    • Updated SettingsFolderExists, CreateSettingsFolder, DeleteSettings, and GetSettingsPath to use GetModuleFolderPath.
    • Corrected SettingsFolderExists, CreateSettingsFolder, and DeleteSettings to use the injected _path.Combine() abstraction instead of calling System.IO.Path.Combine() directly.
  • Utilities.Helper:

    • Added XML documentation to UserLocalAppDataPath and LocalApplicationDataFolder() explaining that UserLocalAppDataPath is mutable and LocalApplicationDataFolder() is dynamically evaluated as a method to support runtime AppData folder overrides (e.g. by MouseWithoutBorders).
  • LanguageModel.cs & UpdatingSettings.cs:

    • Replaced direct Environment.GetFolderPath() calls with SettingsUtils.Default.GetSettingsFilePath() for consistent path resolution.
  • External Callers (FolderUtils.cs & EnvironmentVariablesService.cs):

    • Added Settings.UI.Library.csproj project references to WorkspacesCsharpLibrary.csproj and EnvironmentVariablesUILib.csproj.
    • Updated FolderUtils.DataFolder() to route through SettingsUtils.Default.GetSettingsFilePath("Workspaces", ...).
    • Updated EnvironmentVariablesService constructor to construct its profile path using new SettingPath(_fileSystem.Directory, _fileSystem.Path).GetSettingsPath("EnvironmentVariables", "profiles.json") so path construction respects the injected IFileSystem abstractions.
  • LanguageHelper.cs:

    • Verified that LanguageHelper.cs is in ManagedCommon (which Settings.UI.Library references), so referencing Settings.UI.Library there would introduce a circular dependency. Leaving the direct call to Environment.GetFolderPath() in LanguageHelper.cs to be tracked as a separate architectural issue.
  • Unit Testing:

    • Added SettingPathTests.cs under Settings.UI.UnitTests covering GetSettingsPath (root & module), SettingsFolderExists, CreateSettingsFolder, and DeleteSettings using MockFileSystem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Environment Variables Refers to Environment Variables PowerToys Product-Settings The standalone PowerToys Settings application Product-Workspaces Refers to the Workspaces utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code quality: File paths (for settings)

3 participants