From 8715d0f7e6d34dd42332b8deabf42566eca21a55 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 18 Dec 2016 16:01:28 -0200 Subject: [PATCH] ENH: Add minimal classdef support for MOcovMFile Avoid prepending `mocov_line_covered` to: - `classdef`; - `properties` and `methods` section opening statements; - inside the entire `properties` section, since a limited subset of the M-file syntax is allowed. --- MOcov/@MOcovMFile/MOcovMFile.m | 64 ++++++++++++++--- tests/test_MOcovMFile_recognizes_classdef_syntax.m | 81 ++++++++++++++++++++++ ...nes_with_prefix_generate_valid_classdef_files.m | 68 ++++++++++++++++++ 3 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 tests/test_MOcovMFile_recognizes_classdef_syntax.m create mode 100644 tests/test_write_lines_with_prefix_generate_valid_classdef_files.m diff --git a/MOcov/@MOcovMFile/MOcovMFile.m b/MOcov/@MOcovMFile/MOcovMFile.m index 9de6b2d..866314e 100644 --- a/MOcov/@MOcovMFile/MOcovMFile.m +++ b/MOcov/@MOcovMFile/MOcovMFile.m @@ -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')); \ No newline at end of file + 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')); diff --git a/tests/test_MOcovMFile_recognizes_classdef_syntax.m b/tests/test_MOcovMFile_recognizes_classdef_syntax.m new file mode 100644 index 0000000..0624ce2 --- /dev/null +++ b/tests/test_MOcovMFile_recognizes_classdef_syntax.m @@ -0,0 +1,81 @@ +function test_suite = test_MOcovMFile_recognizes_classdef_syntax + initTestSuite; +end + +function fullname = tempfile(filename, contents) + tempfolder = fullfile(tempdir, 'mocov_fixtures'); + [~, ~, ~] = mkdir(tempfolder); + fullname = fullfile(tempfolder, filename); + fid = fopen(fullname, 'w'); + fprintf(fid, contents); + fclose(fid); +end + +function filename = create_classdef + filename = tempfile('AClass.m', [ ... + 'classdef AClass < handle\n', ... + ' properties\n', ... + ' aProp = 1;\n', ... + ' end\n', ... + ' properties (SetAccess = private, Dependent)\n', ... + ' anotherProp;\n', ... + ' end\n', ... + ' methods\n', ... + ' function self = AClass\n', ... + ' fprintf(''hello world!'');\n', ... + ' end\n', ... + ' end\n', ... + ' methods (Access = private)\n', ... + ' function x = aMethod(self)\n', ... + ' fprintf(''hello world!'');\n', ... + ' end\n', ... + ' end\n', ... + 'end\n' ... + ]); +end + +function test_classdef_line_not_executable + mfile = MOcovMFile(create_classdef); + executable_lines = get_lines_executable(mfile); + assert(~executable_lines(1), ... + '`classdef` line is wrongly classified as executable'); +end + +function test_methods_opening_section_not_executable + mfile = MOcovMFile(create_classdef); + + lines = get_lines(mfile); + executable_lines = get_lines_executable(mfile); + method_opening = [8, 13]; + + for l = method_opening + assert(~executable_lines(l), ... + '`%s` line is wrongly classified as executable', lines{l}); + end +end + +function test_method_body_executable + mfile = MOcovMFile(create_classdef); + + lines = get_lines(mfile); + executable_lines = get_lines_executable(mfile); + method_lines = [10, 15]; + + for l = method_lines + assert(executable_lines(l), ... + '`%s` line is wrongly classified as non-executable', lines{l}); + end +end + +function test_properties_line_not_executable + mfile = MOcovMFile(create_classdef); + + lines = get_lines(mfile); + executable_lines = get_lines_executable(mfile); + properties_lines = [2:4, 5:7]; + + for l = properties_lines + assert(~executable_lines(l), ... + '`%s` line is wrongly classified as executable', lines{l}); + end +end diff --git a/tests/test_write_lines_with_prefix_generate_valid_classdef_files.m b/tests/test_write_lines_with_prefix_generate_valid_classdef_files.m new file mode 100644 index 0000000..e0f5032 --- /dev/null +++ b/tests/test_write_lines_with_prefix_generate_valid_classdef_files.m @@ -0,0 +1,68 @@ +function test_suite = test_write_lines_with_prefix_generate_valid_classdef_files + initTestSuite; +end + +function fullname = tempfile(filename, contents) + tempfolder = fullfile(tempdir, 'mocov_fixtures'); + [~, ~, ~] = mkdir(tempfolder); + fullname = fullfile(tempfolder, filename); + fid = fopen(fullname, 'w'); + fprintf(fid, contents); + fclose(fid); +end + +function filename = create_classdef + filename = tempfile('AClass.m', [ ... + 'classdef AClass < handle\n', ... + ' properties\n', ... + ' aProp = 1;\n', ... + ' end\n', ... + ' properties (SetAccess = private, Dependent)\n', ... + ' anotherProp;\n', ... + ' end\n', ... + ' methods\n', ... + ' function self = AClass\n', ... + ' self.anotherProp = 2;\n', ... + ' end\n', ... + ' end\n', ... + ' methods (Access = public)\n', ... + ' function aMethod(self, x)\n', ... + ' self.aProp = x;\n', ... + ' end\n', ... + ' end\n', ... + 'end\n' ... + ]); +end + +function test_generate_valid_file + originalPath = path; % setup + cleaner = onCleanup(@() path(originalPath)); % teardown + + % Given: + % `AClass.m` file with a classdef declaration + filename = create_classdef; + % a folder where mocov will store the decorated files + foldername = fullfile(tempdir, 'mocov_decorated'); + [~,~,~] = mkdir(foldername); + decorated = fullfile(foldername, 'AClass.m'); + % a valid decorator + decorator = @(line_number) ... + sprintf('fprintf(0, ''%s:%d'');', filename, line_number); + + + % When: the decorated file is generated + mfile = MOcovMFile(filename); + write_lines_with_prefix(mfile, decorated, decorator); + + + % Then: the decorated file should have a valid syntax + % Since Octave do not have a linter, run the code to check the syntax. + addpath(foldername); + try + aObject = AClass(); + aObject.aMethod(4); + catch + assert(false, ['Problems when running the decorated file: `%s` ', ... + 'please check for syntax errors.'], decorated); + end +end