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 @@
-
+
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", ...