From d86f78895845515d2a9b0a49dbedddb060643814 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 15 Oct 2026 22:34:33 +0200 Subject: [PATCH 1/2] feat: warn about namespace references that vendorPackage cannot rename vendorPackage renames the source namespace only in .m files, and only where "." and a letter or "*" follow the name. A namespace name built at run time, such as "webprogress." + name or meta.package.fromName("webprogress"), and every reference in another file, such as an .mlx file or functionSignatures.json, was left unchanged without notice. In the copy, such a reference resolves to the original namespace: it fails when that namespace is not installed, and runs another version of the code when it is. After renaming, the copied files are now searched for the source namespace name as a whole word, and the warning MatBox:VendorPackage:UnrenamedReferences lists each place found: file and line for text files, the file alone for binary files. .mlx and .mlapp files are searched in the files their zip archive holds. Error identifiers and comment lines of .m files are not reported, and the license file and vendorinfo.json are added after the search. Co-Authored-By: Claude Opus 5.5 --- .../+internal/findNamespaceReferences.m | 119 ++++++++++++++ .../+internal/vendorPackageFromFolder.m | 17 ++ code/+matbox/+tasks/vendorPackage.m | 8 + .../+unittest/VendorPackageTest.m | 152 ++++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 code/+matbox/+tasks/+internal/findNamespaceReferences.m diff --git a/code/+matbox/+tasks/+internal/findNamespaceReferences.m b/code/+matbox/+tasks/+internal/findNamespaceReferences.m new file mode 100644 index 0000000..f416d7c --- /dev/null +++ b/code/+matbox/+tasks/+internal/findNamespaceReferences.m @@ -0,0 +1,119 @@ +function locations = findNamespaceReferences(folderPath, namespaceName) +%findNamespaceReferences - Find the places where the files below a folder name a namespace +% locations = matbox.tasks.internal.findNamespaceReferences(folderPath,namespaceName) +% searches the files below folderPath for namespaceName as a whole word +% and returns a string column vector with one element per place found. +% For a text file, an element is the path of the file relative to +% folderPath, with "/" as separator, followed by ":" and the line +% number, such as "upload.m:12". For a binary file, it is the path +% alone, such as "examples/demo.mlx". +% +% The name is not counted when a letter, digit, "_", ".", "/" or "\" +% comes before it, as in matbox.tasks.internal.renameNamespaceInText, or +% when a letter, digit, "_" or ":" comes after it. This leaves out longer +% names, renamed references ("mytoolbox.external.webprogress.upload"), +% paths and error identifiers ("webprogress:upload:Failed"). Lines of .m +% files that are comments are not searched. +% +% .mlx and .mlapp files are zip archives and are searched in the files +% they hold. A file with a NUL byte is binary and is searched as it is +% stored, so a name in compressed data, such as a MAT-file saved in the +% default format, is not found. +% +% See also matbox.tasks.vendorPackage, matbox.tasks.internal.renameNamespaceInText + + arguments + folderPath (1,1) string {mustBeFolder} + namespaceName (1,1) string {mustBeNonzeroLengthText} + end + + ARCHIVE_EXTENSIONS = [".mlx", ".mlapp"]; + CODE_EXTENSION = ".m"; + + % The character classes are spelled out because \w may also match + % non-ASCII letters, and the text is the raw bytes of a file. + notInsideLongerName = "(?x = webprogress.upload(a);") + zip(fullfile(pwd, "demo.zip"), "matlab", contentFolder) + folderPath = fullfile(pwd, "package"); + mkdir(fullfile(folderPath, "examples")) + movefile(fullfile(pwd, "demo.zip"), fullfile(folderPath, "examples", "demo.mlx")) + + locations = matbox.tasks.internal.findNamespaceReferences( ... + folderPath, testCase.SourceNamespace); + + testCase.verifyEqual(locations, "examples/demo.mlx") + end + + function testNameInBinaryFileIsFoundByFileName(testCase) + folderPath = fullfile(pwd, "package"); + writeText(fullfile(folderPath, "data.bin"), ... + char(0) + "webprogress.upload" + char(0)) + + locations = matbox.tasks.internal.findNamespaceReferences( ... + folderPath, testCase.SourceNamespace); + + testCase.verifyEqual(locations, "data.bin") + end + end + methods (Test) % vendorPackageFromFolder function testFilesAreCopiedAndRenamed(testCase) repositoryFolder = testCase.createRepository(); @@ -400,6 +493,52 @@ function testBytesOutsideReferencesArePreserved(testCase) testCase.verifyEqual(readBytes(fullfile(targetFolder, "scale.m")), ... unicode2native(expectedText, "UTF-8")) end + + function testStringBuiltReferenceWarns(testCase) + repositoryFolder = testCase.createRepository(); + writeText(fullfile(repositoryFolder, testCase.SourceFolder, "createMonitor.m"), ... + join([ + "function monitor = createMonitor(name)" + " monitor = feval(""webprogress."" + name);" + "end" + ], newline)) + targetFolder = testCase.getTargetFolder(); + + testCase.verifyWarning( ... + @() matbox.tasks.internal.vendorPackageFromFolder( ... + repositoryFolder, testCase.SourceFolder, targetFolder), ... + "MatBox:VendorPackage:UnrenamedReferences") + + testCase.verifySubstring(lastwarn(), "createMonitor.m:2") + testCase.verifyTrue(isfile(fullfile(targetFolder, "createMonitor.m"))) + end + + function testFunctionSignaturesReferenceWarns(testCase) + repositoryFolder = testCase.createRepository(); + writeText(fullfile(repositoryFolder, testCase.SourceFolder, ... + "resources", "functionSignatures.json"), testCase.createFunctionSignatures()) + targetFolder = testCase.getTargetFolder(); + + testCase.verifyWarning( ... + @() matbox.tasks.internal.vendorPackageFromFolder( ... + repositoryFolder, testCase.SourceFolder, targetFolder), ... + "MatBox:VendorPackage:UnrenamedReferences") + + testCase.verifySubstring(lastwarn(), "resources/functionSignatures.json:3") + end + + function testCopyWithoutUnrenamedReferencesDoesNotWarn(testCase) + % The license file and vendorinfo.json name the source namespace + % without referring to it, and are not searched. + repositoryFolder = testCase.createRepository(); + writeText(fullfile(repositoryFolder, "LICENSE"), ... + "Copyright (c) the authors of webprogress") + targetFolder = testCase.getTargetFolder(); + + testCase.verifyWarningFree( ... + @() matbox.tasks.internal.vendorPackageFromFolder( ... + repositoryFolder, testCase.SourceFolder, targetFolder)) + end end methods (Test) % vendorPackage @@ -453,6 +592,19 @@ function testVendorFromGitHub(testCase) "function mustBeValidUrl(~), end") end + function signaturesText = createFunctionSignatures(~) + % Text of a functionSignatures.json file that names a function + % of SourceNamespace on line 3 + signaturesText = join([ + "{" + " ""_schemaVersion"": ""1.0.0""," + " ""webprogress.upload"": {" + " ""inputs"": []" + " }" + "}" + ], newline); + end + function targetFolder = getTargetFolder(~) % Full path of a folder that defines the namespace TargetNamespace targetFolder = string(fullfile(pwd, "project", "src", ... From 32a1c4bcbbae140bacff1027cb2e71d6461cd45f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:40:03 +0000 Subject: [PATCH 2/2] Update GitHub badges [skip ci] --- .github/badges/tests.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg index 828f2d7..445b28b 100644 --- a/.github/badges/tests.svg +++ b/.github/badges/tests.svg @@ -1 +1 @@ -tests: 83 passedtests83 passed +tests: 99 passedtests99 passed