From 67ae9343b50c2d67fcba96070d185b10af61cda3 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 14 May 2026 11:18:24 +0200 Subject: [PATCH 1/2] refactor: replace eval-based dynamic field access with MATLAB idioms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace eval(strcat('S.', name)) and eval(strjoin({...}, '.')) patterns with direct dynamic field access (S.(name)) and subsref for dotted paths. Remove nansen.manage.OptionsAdapter — dead duplicate of nansen.wrapper.abstract.OptionsAdapter with zero subclasses or callers. Co-Authored-By: Claude Sonnet 4.6 --- code/+nansen/+config/SessionMethodsCatalog.m | 3 +- code/+nansen/+manage/OptionsAdapter.m | 91 ------------------- code/apps/+structeditor/App.m | 2 +- code/general/+tools/editStruct.m | 4 +- .../+wrapper/+abstract/OptionsAdapter.m | 5 +- 5 files changed, 7 insertions(+), 98 deletions(-) delete mode 100644 code/+nansen/+manage/OptionsAdapter.m diff --git a/code/+nansen/+config/SessionMethodsCatalog.m b/code/+nansen/+config/SessionMethodsCatalog.m index f9dcd06c3..9ef63c920 100644 --- a/code/+nansen/+config/SessionMethodsCatalog.m +++ b/code/+nansen/+config/SessionMethodsCatalog.m @@ -25,8 +25,7 @@ function S = getDefaultItem() %getDefaultItem Get default item for catalog - S = eval( sprintf('%s.getBlankItem()', mfilename('class')) ); - + S = feval([mfilename('class'), '.getBlankItem']); % Note: No default exists for this item type, so returning the % blank item end diff --git a/code/+nansen/+manage/OptionsAdapter.m b/code/+nansen/+manage/OptionsAdapter.m deleted file mode 100644 index 75b49d6b6..000000000 --- a/code/+nansen/+manage/OptionsAdapter.m +++ /dev/null @@ -1,91 +0,0 @@ -classdef OptionsAdapter < handle -%OptionsAdapter Adapter for toolbox options -% -% Superclass that defines properties and methods that are required for an -% OptionsAdapter subclass. -% -% An OptionsAdaper class should define an options struct with clear names -% that can be inputted to the struct editor app. It should also convert -% these options to the format required by its corresponding toolbox. - - % Question: - % 1. Why are methods in general static? - % So that we can get preset options without creating a class - % instance... But what is the benefit??? - % - % 2. Why is the getToolboxOptions method static? - % - % 3. Should the Options property provide the default options struct - % on demand - - properties (Abstract, Constant) - ToolboxName % Name of toolbox this options adapter correspond with - Name % Name of options (For keeping track of options presets/variations) - Description % Description for an option preset/variation - end - - properties (Dependent) - Options % A struct of options. Not sure if this should be stored in the class... - end - - methods (Abstract, Static) - S = getOptions() % For nansen/ui options - S = convert(S) % For conversion to toolbox options - - %S = getAdapter() % Adapter for converting options to toolbox names. - end - - methods - - function S = get.Options(obj) - S = obj.getOptions(); - end - end - - methods (Static) - - function SOut = rename(S, nameMap, outputFormat) - - if nargin < 3|| isempty(outputFormat) - outputFormat = 'struct'; % vs nvpairs - end - - % Get fieldnames recursively and find intersection - fieldsOpts = fieldnamesr(S, 2); - fieldsNames = fieldnamesr(nameMap); - - C = intersect(fieldsOpts, fieldsNames); - - switch outputFormat - case 'struct' - SOut = struct(); - - for i = 1:numel(C) - - subfields = strsplit(C{i}, '.'); - s = struct('type', {'.'}, 'subs', subfields); - - name = subsref(nameMap, s); - value = subsref(S, s); - - s = struct('type', {'.'}, 'subs', name); - SOut = subsasgn(SOut, s, value); - end - - case 'nvpairs' - - % Todo: O - % Collect normcorre parameter names and values in a cell array - % of name-value pairs. - nvPairs = cell(1, numel(C)*2); - for i = 1:numel(C) - name = eval(strjoin({'nameMap', C{i}}, '.')); - value = eval(strjoin({'S', C{i}}, '.')); - ind = (i-1)*2 + (1:2); - nvPairs(ind) = {name, value}; - end - SOut = nvPairs; - end - end - end -end diff --git a/code/apps/+structeditor/App.m b/code/apps/+structeditor/App.m index 56e44c107..72f79b495 100644 --- a/code/apps/+structeditor/App.m +++ b/code/apps/+structeditor/App.m @@ -1490,7 +1490,7 @@ function addComponents(obj, panelNum) y = y + obj.RowHeight; otherwise - val = eval(strcat('S', '.', currentProperty)); + val = S.(currentProperty); [yCorrTmp, wasAborted] = obj.newInputField(contentPanel, y, currentProperty, val, config, tip); if wasAborted; continue; end diff --git a/code/general/+tools/editStruct.m b/code/general/+tools/editStruct.m index 012e7174f..834cead76 100644 --- a/code/general/+tools/editStruct.m +++ b/code/general/+tools/editStruct.m @@ -207,14 +207,14 @@ function createComponents(guiFig, S, fieldNames) for i = 1:numel(propertyFields) currentField = propertyFields{i}; name = strcat(currentProperty, '.', currentField); - val = eval(strcat('S', '.', name)); + val = S.(currentProperty).(currentField); addInputField(guiPanel, y, name, val) y = y + rowHeight + rowSep; end end otherwise - val = eval(strcat('S', '.', currentProperty)); + val = S.(currentProperty); addInputField(guiPanel, y, currentProperty, val) y = y + rowHeight + rowSep; end diff --git a/code/wrappers/+nansen/+wrapper/+abstract/OptionsAdapter.m b/code/wrappers/+nansen/+wrapper/+abstract/OptionsAdapter.m index 2f7d8f623..3dd4e78ee 100644 --- a/code/wrappers/+nansen/+wrapper/+abstract/OptionsAdapter.m +++ b/code/wrappers/+nansen/+wrapper/+abstract/OptionsAdapter.m @@ -79,8 +79,9 @@ nvPairs = cell(1, numel(C)*2); for i = 1:numel(C) - name = eval(strjoin({'nameMap', C{i}}, '.')); - value = eval(strjoin({'S', C{i}}, '.')); + s = struct('type', {'.'}, 'subs', strsplit(C{i}, '.')); + name = subsref(nameMap, s); + value = subsref(S, s); ind = (i-1)*2 + (1:2); nvPairs(ind) = {name, value}; end From 8700719c4fdee870b31e400b5dac4e0cd41587e4 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 14 May 2026 11:32:35 +0200 Subject: [PATCH 2/2] refactor: replace eval-based Constant property access with helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add nansen.internal.introspection.getConstantPropertyValue(className, propName) as a safe replacement for eval(sprintf('%s.PropName', className)). Uses meta.class.fromName internally and errors clearly when the class or property is not found or not Constant. Replace 9 call sites across Preferences, NansenUserSession, ImageStack, MetaTable, MetaTableColumnLayout, formatTableForDisplay, OptionsManager (×2), and App. Co-Authored-By: Claude Sonnet 4.6 --- .../+fileadapter/+imagestack/ImageStack.m | 2 +- .../+introspection/getConstantPropertyValue.m | 40 +++++++++++++++++++ .../@NansenUserSession/NansenUserSession.m | 2 +- code/+nansen/+internal/+user/Preferences.m | 2 +- code/+nansen/+manage/OptionsManager.m | 6 +-- .../+utility/formatTableForDisplay.m | 2 +- code/+nansen/+metadata/@MetaTable/MetaTable.m | 2 +- code/+nansen/+ui/MetaTableColumnLayout.m | 2 +- code/apps/+nansen/@App/App.m | 2 +- 9 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 code/+nansen/+internal/+introspection/getConstantPropertyValue.m diff --git a/code/+nansen/+dataio/+fileadapter/+imagestack/ImageStack.m b/code/+nansen/+dataio/+fileadapter/+imagestack/ImageStack.m index 31e8d3b71..87243b515 100644 --- a/code/+nansen/+dataio/+fileadapter/+imagestack/ImageStack.m +++ b/code/+nansen/+dataio/+fileadapter/+imagestack/ImageStack.m @@ -137,7 +137,7 @@ function uifind(obj, varargin) for i = 1:numel(virtualDataClasses) thisClassName = virtualDataClasses{i}; - fileNameExpression = eval([thisClassName, '.FilenameExpression']); + fileNameExpression = nansen.internal.introspection.getConstantPropertyValue(thisClassName, 'FilenameExpression'); if ~isempty( regexp(filename, fileNameExpression, 'once') ) className = thisClassName; diff --git a/code/+nansen/+internal/+introspection/getConstantPropertyValue.m b/code/+nansen/+internal/+introspection/getConstantPropertyValue.m new file mode 100644 index 000000000..0cf00d8bf --- /dev/null +++ b/code/+nansen/+internal/+introspection/getConstantPropertyValue.m @@ -0,0 +1,40 @@ +function value = getConstantPropertyValue(className, propertyName) +% getConstantPropertyValue - Get the value of a Constant property by class name string. +% +% value = nansen.internal.introspection.getConstantPropertyValue(className, propertyName) +% returns the value of the Constant property named propertyName on the class +% identified by the string className. This is the safe replacement for +% eval(sprintf('%s.%s', className, propertyName)). +% +% Input Arguments: +% className - Fully qualified class name (string or char) +% propertyName - Name of a Constant property on that class (string or char) +% +% Output Arguments: +% value - Value of the Constant property + + arguments + className (1,1) string + propertyName (1,1) string + end + + mc = meta.class.fromName(className); + if isempty(mc) + error('nansen:introspection:ClassNotFound', ... + 'Class "%s" was not found. Ensure the class is on the MATLAB path.', className) + end + + matchIdx = strcmp({mc.PropertyList.Name}, propertyName); + if ~any(matchIdx) + error('nansen:introspection:PropertyNotFound', ... + 'Class "%s" has no property named "%s".', className, propertyName) + end + + prop = mc.PropertyList(matchIdx); + if ~prop.Constant + error('nansen:introspection:NotConstant', ... + 'Property "%s.%s" is not Constant.', className, propertyName) + end + + value = prop.DefaultValue; +end diff --git a/code/+nansen/+internal/+user/@NansenUserSession/NansenUserSession.m b/code/+nansen/+internal/+user/@NansenUserSession/NansenUserSession.m index d492a70ee..101768cfd 100644 --- a/code/+nansen/+internal/+user/@NansenUserSession/NansenUserSession.m +++ b/code/+nansen/+internal/+user/@NansenUserSession/NansenUserSession.m @@ -305,7 +305,7 @@ function runPostConstructionUpdateActions(obj) if ~nargin || isempty(userName) warning('No username given, returning prefdir for default user') className = mfilename('class'); - userName = eval(sprintf('%s.DEFAULT_USER_NAME', className)); + userName = nansen.internal.introspection.getConstantPropertyValue(className, 'DEFAULT_USER_NAME'); end preferenceDirectory = fullfile(prefdir, 'Nansen', userName); end diff --git a/code/+nansen/+internal/+user/Preferences.m b/code/+nansen/+internal/+user/Preferences.m index 88c6a38b4..0d2cea184 100644 --- a/code/+nansen/+internal/+user/Preferences.m +++ b/code/+nansen/+internal/+user/Preferences.m @@ -42,7 +42,7 @@ function filename = createFilename() %Create filename for a preference file. classname = mfilename('class'); - prefGroupName = eval(sprintf('%s.PreferenceGroupName', classname)); + prefGroupName = nansen.internal.introspection.getConstantPropertyValue(classname, 'PreferenceGroupName'); prefGroupName = matlab.lang.makeValidName(prefGroupName); filename = fullfile(sprintf('%s_Preferences.mat', prefGroupName)); end diff --git a/code/+nansen/+manage/OptionsManager.m b/code/+nansen/+manage/OptionsManager.m index 66e0309db..04e7f47e0 100644 --- a/code/+nansen/+manage/OptionsManager.m +++ b/code/+nansen/+manage/OptionsManager.m @@ -956,14 +956,12 @@ function updateOptionsFromDefault(obj) fcnName = strcat(obj.FunctionName, '.getDefaultOptions'); fcnHandle = str2func(fcnName); - methodNameFcnName = strcat(obj.FunctionName, '.MethodName'); - % Return as options entry (struct) opts = fcnHandle(); name = 'Preset Options'; try - methodName = eval(methodNameFcnName); + methodName = nansen.internal.introspection.getConstantPropertyValue(obj.FunctionName, 'MethodName'); catch methodName = obj.FunctionName; end @@ -1010,7 +1008,7 @@ function updateOptionsFromDefault(obj) function optionsEntry = getPresetsFromSuperclass(obj) %getPresetsFromSuperclass - optManager = eval(sprintf('%s.OptionsManager', obj.FunctionName)); + optManager = nansen.internal.introspection.getConstantPropertyValue(obj.FunctionName, 'OptionsManager'); presetOptionsNames = optManager.PresetOptionNames; for i = 1:numel(presetOptionsNames) diff --git a/code/+nansen/+metadata/+utility/formatTableForDisplay.m b/code/+nansen/+metadata/+utility/formatTableForDisplay.m index 4d49d5709..8abdd8b6c 100644 --- a/code/+nansen/+metadata/+utility/formatTableForDisplay.m +++ b/code/+nansen/+metadata/+utility/formatTableForDisplay.m @@ -72,7 +72,7 @@ % Step 3: does the data type have it's own formatter? dataHasTableFormatter = cellfun(@(c) isa(c, 'nansen.metadata.tablevar.mixin.HasTableColumnFormatter'), firstRowData); formattingFcn(dataHasTableFormatter) = cellfun(@(c) ... - str2func(class(eval( strjoin({class(c), 'TableColumnFormatter'}, '.')))), ... + str2func(class(nansen.internal.introspection.getConstantPropertyValue(class(c), 'TableColumnFormatter'))), ... firstRowData(dataHasTableFormatter), 'uni', 0); % Step 4: Format all the table columns that needs formatting diff --git a/code/+nansen/+metadata/@MetaTable/MetaTable.m b/code/+nansen/+metadata/@MetaTable/MetaTable.m index 80bc1648f..5ece69d50 100644 --- a/code/+nansen/+metadata/@MetaTable/MetaTable.m +++ b/code/+nansen/+metadata/@MetaTable/MetaTable.m @@ -135,7 +135,7 @@ function markClean(obj) schemaIdName = obj.MetaTableIdVarname; else try - schemaIdName = eval(strjoin({obj.MetaTableClass, 'IDNAME'}, '.')); + schemaIdName = nansen.internal.introspection.getConstantPropertyValue(obj.MetaTableClass, 'IDNAME'); catch schemaIdName = 'id'; end diff --git a/code/+nansen/+ui/MetaTableColumnLayout.m b/code/+nansen/+ui/MetaTableColumnLayout.m index d197d8f96..98d1b98f8 100644 --- a/code/+nansen/+ui/MetaTableColumnLayout.m +++ b/code/+nansen/+ui/MetaTableColumnLayout.m @@ -734,7 +734,7 @@ function checkAndUpdateColumnEntries(obj) if isa(dataValue, 'nansen.metadata.abstract.TableVariable') %|| isa(dataValue, 'nansen.metadata.tablevar.mixin.HasTableColumnFormatter') if isempty(dataValue) - isEditable = eval(sprintf('%s.IS_EDITABLE', class(dataValue))); + isEditable = nansen.internal.introspection.getConstantPropertyValue(class(dataValue), 'IS_EDITABLE'); else isEditable = dataValue.IS_EDITABLE; end diff --git a/code/apps/+nansen/@App/App.m b/code/apps/+nansen/@App/App.m index 610087bca..d6ddae6c8 100644 --- a/code/apps/+nansen/@App/App.m +++ b/code/apps/+nansen/@App/App.m @@ -3641,7 +3641,7 @@ function onSessionTaskSelected(app, ~, evt) return elseif strcmp(evt.Mode, 'Help') try - titleStr = eval(sprintf('%s.MethodName', evt.TaskAttributes.FunctionName)); + titleStr = nansen.internal.introspection.getConstantPropertyValue(evt.TaskAttributes.FunctionName, 'MethodName'); catch if ~isempty(evt.TaskAttributes.MethodName) titleStr = evt.TaskAttributes.MethodName;