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