Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

ENH: Add minimal classdef support for MOcovMFile #9

Merged
merged 6 commits into from Dec 24, 2016
@@ -32,9 +32,14 @@
lines=regexp(s,sprintf('\n'),'split');
- % whether the previous line contained a line continuation,
- % i.e. ended with '...'
- in_line_continuation=false;
+ % state variables
+ in_line_continuation = false; % whether the previous line contained a
+ % line continuation, i.e. ended with '...'
+
+ inside_class = false; % the file contains a `classdef` statement
+
+ inside_properties = false; % a `properties` section was opened
+ % but not closed.
% see which lines are executable
n=numel(lines);
@@ -57,12 +62,33 @@
continue;
end
- executable(k)=~(in_line_continuation || ...
- line_is_function_def(line_code_trimmed) || ...
- line_is_case_statement(line_code) || ...
- line_is_elseif_statement(line_code) || ...
- line_is_else_statement(line_code) || ...
- line_is_sole_end_statement(line_code_trimmed));
+ % Check for statements that ~changes/depends on~ the 'parser' state
+ classdef_statement = line_is_class_def(line_code_trimmed);
+ inside_class = inside_class || classdef_statement;
+
+ properties_section = line_opens_properties_section(...
+ line_code_trimmed, inside_class);
+ % When a properties section is opened set the inside_properties flag
+ inside_properties = inside_properties || properties_section;
+ % When inside_properties is set and an 'end' appears, the section
+ % is closed.
+ % This can be considered a hack, since assumes no code that includes
+ % an `end` is allowed inside properties.
+ inside_properties = inside_properties && ...
+ ~line_ends_with_end_statement(line_code_trimmed);
+
+ % classify line
+ executable(k)=~(...
+ in_line_continuation || ...
+ line_is_function_def(line_code_trimmed) || ...
+ classdef_statement || ...
+ line_opens_methods_section(line_code_trimmed, inside_class) || ...
+ inside_properties || ... % Arbitrary code cannot run inside
+ ... % properties section, just a subset
+ line_is_case_statement(line_code) || ...
+ line_is_elseif_statement(line_code) || ...
+ line_is_else_statement(line_code) || ...
+ line_is_sole_end_statement(line_code_trimmed));
in_line_continuation=numel(line_code_trimmed)>=3 && ...
strcmp(line_code_trimmed(end+(-2:0)),'...');
@@ -84,6 +110,20 @@
tf=~isempty(regexp([newline line],pat,'once'));
+function tf=line_is_class_def(line)
+ % returns true if the line opens a class definition
+ tf=~isempty(regexp(line,'^\s*classdef\W*','once'));
+
+function tf=line_opens_methods_section(line, inside_class)
+ % returns true if the line opens a method section inside class definition
+ tf=inside_class && ...
+ ~isempty(regexp(line,'^\s*methods\s*(\([^\(\)]*\))?\s*$','once'));
+
+function tf=line_opens_properties_section(line, inside_class)
+ % returns true if the line opens a properties section inside class definition
+ tf=inside_class && ...
+ ~isempty(regexp(line,'^\s*properties\s*(\([^\(\)]*\))?\s*$','once'));
+
function tf=line_is_sole_end_statement(line)
% returns true if the string in line is just an end statement
tf=isempty(regexprep(line,'([\s,;]|^)?end([\s,;]|$)?',''));
@@ -95,4 +135,8 @@
tf=~isempty(regexp(line,'^\s*elseif\W*','once'));
function tf=line_is_else_statement(line)
- tf=~isempty(regexp(line,'^\s*else\W*','once'));
+ tf=~isempty(regexp(line,'^\s*else\W*','once'));
+
+function tf=line_ends_with_end_statement(line)
+ % returns true if the line has a finishing 'end'
+ tf=~isempty(regexp(line,'(^|\W)end\s*[,;]?\s*$','once'));
@@ -0,0 +1,157 @@
+function test_suite = test_MOcovMFile_recognizes_classdef_syntax
+ initTestSuite;
+end
+
+function assertStringContains(text, subtext)
+ assert(~isempty(strfind(text, subtext)), ...
+ 'String ''%s'' should contain ''%s'', but it doesn''t.', text, subtext);
+end
+
+function filepath = create_tempfile(filename, contents)
+ % Creates a temporary file with the specified content.
+
+ filepath = fullfile(tempdir, filename);
+ fid = fopen(filepath, 'w');
+ fprintf(fid, contents);
+ fclose(fid);
+end
+
+function filepath = create_classdef(classname)
+ if nargin < 1
+ % Use a random name to ensure uniqueness
+ classname = char(64 + ceil(26*rand(1, 20)));
+ end
+
+ filepath = create_tempfile([classname, '.m'], [ ...
+ 'classdef ', classname, ' < handle\n', ...
+ ' properties\n', ...
+ ' aProp;\n', ...
+ ' end\n', ...
+ ' properties (SetAccess = private, Dependent)\n', ...
+ ' anotherProp;\n', ...
+ ' end\n', ...
+ ' methods\n', ...
+ ' function self = ', classname, ' \n', ...
+ ' fprintf(0, ''hello world!'');\n', ...
+ ' end\n', ...
+ ' end\n', ...
+ ' methods (Access = public)\n', ...
+ ' function x = aMethod(self)\n', ...
+ ' fprintf(0, ''hello world!'');\n', ...
+ ' end\n', ...
+ ' end\n', ...
+ 'end\n' ...
+ ]);
+end
+
+function test_classdef_line_not_executable
+ % Test subject: `MOcovMFile` constructor
+
+ tempfile = create_classdef;
+ teardown = onCleanup(@() delete(tempfile));
+
+ mfile = MOcovMFile(tempfile);
+ lines = get_lines(mfile);
+ executable_lines = get_lines_executable(mfile);
+
+ assertStringContains(lines{1}, 'classdef');
+ assert(~executable_lines(1), ...
+ '`classdef` line is wrongly classified as executable');
+end
+
+function test_methods_opening_section_not_executable
+ % Test subject: `MOcovMFile` constructor
+
+ tempfile = create_classdef;
+ teardown = onCleanup(@() delete(tempfile));
+
+ mfile = MOcovMFile(tempfile);
+ lines = get_lines(mfile);
+ executable_lines = get_lines_executable(mfile);
+ method_opening = [8, 13];
+
+ for n = method_opening
+ assertStringContains(lines{n}, 'methods');
+ assert(~executable_lines(n), ...
+ '`%s` line is wrongly classified as executable', lines{n});
+ end
+end
+
+function test_method_body_executable
+ % Test subject: `MOcovMFile` constructor
+
+ tempfile = create_classdef;
+ teardown = onCleanup(@() delete(tempfile));
+
+ mfile = MOcovMFile(tempfile);
+ lines = get_lines(mfile);
+ executable_lines = get_lines_executable(mfile);
+ method_lines = [10, 15];
+
+ for n = method_lines
+ assertStringContains(lines{n}, 'fprintf');
+ assert(executable_lines(n), ...
+ '`%s` line is wrongly classified as non-executable', lines{n});
+ end
+end
+
+function test_properties_line_not_executable
+ % Test subject: `MOcovMFile` constructor
+
+ tempfile = create_classdef;
+ teardown = onCleanup(@() delete(tempfile));
+
+ mfile = MOcovMFile(tempfile);
+ lines = get_lines(mfile);
+ executable_lines = get_lines_executable(mfile);
+ properties_opening = [2, 5];
+ properties_body = [3, 6];
+
+ for n = properties_opening
+ assertStringContains(lines{n}, 'properties');
+ assert(~executable_lines(n), ...
+ '`%s` line is wrongly classified as executable', lines{n});
+ end
+
+ for n = properties_body;
+ assertStringContains(lines{n}, 'Prop;');
+ assert(~executable_lines(n), ...
+ '`%s` line is wrongly classified as executable', lines{n});
+ end
+end
+
+function test_generate_valid_file
+ % Test subject: `write_lines_with_prefix` method
+
+ original_path = path;
+ path_cleanup = onCleanup(@() path(original_path));
+
+ % Given:
+
+ % `AClass.m` file with a classdef declaration
+ classname = ['AClass', char(64 + ceil(26*rand(1, 20)))];
+ tempfile = create_classdef(classname);
+ teardown = onCleanup(@() delete(tempfile));
+ % a valid decorator
+ decorator = @(line_number) ...
+ sprintf('fprintf(0, ''%s:%d'');', tempfile, line_number);
+
+ % When: the decorated file is generated
+ mfile = MOcovMFile(tempfile);
+ write_lines_with_prefix(mfile, tempfile, decorator);
+ % ^ Here we just overwrite the original file, because we don't use it.
+ % In the real word, the new file should be saved to a
+ % different folder.
+
+ % Then: the decorated file should have a valid syntax
+ % Since Octave do not have a linter, run the code to check the syntax.
+ addpath(tempdir);
+ try
+ constructor = str2func(classname);
+ aObject = constructor();
+ aObject.aMethod();
+ catch
+ assert(false, ['Problems when running the decorated file: `%s` ', ...
+ 'please check for syntax errors.'], decorated);
+ end
+end