diff --git a/src/lib/c/statistics.c b/src/lib/c/statistics.c index ba5e8df..347aa44 100644 --- a/src/lib/c/statistics.c +++ b/src/lib/c/statistics.c @@ -250,10 +250,11 @@ static inline bool count( RCN_LOG_DBG(file->path) bool ok = false; + result->hasLogicalLines = detected.isProgrammingLanguage; RcnTextFormat sourceFormat = detected.format; ok = ensureFileContent(stats, options, file, result); if (ok && options.operations & RCN_OPT_COUNT_LOGICAL_LINES){ - if (detected.isProgrammingLanguage) { + if (result->hasLogicalLines) { ok = countLogicalLines(stats, file, sourceFormat, result); } } diff --git a/src/lib/include/reckon/reckon.h b/src/lib/include/reckon/reckon.h index 856768e..b8ba7c8 100644 --- a/src/lib/include/reckon/reckon.h +++ b/src/lib/include/reckon/reckon.h @@ -347,6 +347,19 @@ typedef struct RcnCountResultGroup { */ bool isProcessed; + /** + * Indicates whether logical lines can be computed for the source entity. + * + * If `true`, the `logicalLines` field contains a valid count. If `false`, + * then logical lines are not applicable for the source entity's format, + * e.g. for plain text files, and the `logicalLines` field is zero. + * This field is only set by a counting operation and remains initialized + * as `false` if no such operation was performed. + * + * @since 1.1.0 + */ + bool hasLogicalLines; + } RcnCountResultGroup; /** diff --git a/src/lib/tests/unit/c/test_statistics.c b/src/lib/tests/unit/c/test_statistics.c index 4786746..425ba3d 100644 --- a/src/lib/tests/unit/c/test_statistics.c +++ b/src/lib/tests/unit/c/test_statistics.c @@ -219,6 +219,31 @@ void testCountWithMultipleFilesWhenOneFileHasError(void) { rcnFreeCountStatistics(stats); } +void testCountResultGroupLogicalLineCheckField(void) { + char* path = RECKON_TEST_PATH_RES_BASE "/mixed"; + RcnCountStatistics* stats = rcnCreateCountStatistics(path); + RcnStatOptions options = {0}; + rcnCount(stats, options); + TEST_ASSERT_EQUAL_INT(4, stats->count.size); + RcnSourceFile* fileJava = &stats->count.files[0]; + RcnCountResultGroup* resultJava = &stats->count.results[0]; + TEST_ASSERT_EQUAL_STRING("Source.java", fileJava->name); + TEST_ASSERT_TRUE(resultJava->hasLogicalLines); + RcnSourceFile* fileC = &stats->count.files[1]; + RcnCountResultGroup* resultC = &stats->count.results[1]; + TEST_ASSERT_EQUAL_STRING("source.c", fileC->name); + TEST_ASSERT_TRUE(resultC->hasLogicalLines); + RcnSourceFile* fileTxt = &stats->count.files[2]; + RcnCountResultGroup* resultTxt = &stats->count.results[2]; + TEST_ASSERT_EQUAL_STRING("text.txt", fileTxt->name); + TEST_ASSERT_FALSE(resultTxt->hasLogicalLines); + RcnSourceFile* fileMd = &stats->count.files[3]; + RcnCountResultGroup* resultMd = &stats->count.results[3]; + TEST_ASSERT_EQUAL_STRING("text2.md", fileMd->name); + TEST_ASSERT_FALSE(resultMd->hasLogicalLines); + rcnFreeCountStatistics(stats); +} + // NOLINTEND(readability-magic-numbers) int main(void) { @@ -229,5 +254,6 @@ int main(void) { RUN_TEST(testCountWithFileWhenContentIsNullAndStatusIsFileError); RUN_TEST(testCountWhenFileHasUnsupportedFormat); RUN_TEST(testCountWithMultipleFilesWhenOneFileHasError); + RUN_TEST(testCountResultGroupLogicalLineCheckField); return UNITY_END(); } diff --git a/src/scount/c/print.c b/src/scount/c/print.c index 12703c4..7bc9993 100644 --- a/src/scount/c/print.c +++ b/src/scount/c/print.c @@ -44,6 +44,7 @@ static const char TABLE_BORDER_VERTICAL_NORMAL = '|'; static const char TABLE_BORDER_VERTICAL_EMPHASIS = '|'; static const char TABLE_BORDER_CORNER = 'o'; static const char* TABLE_PADDING_LEFT = " "; +static const char* LABEL_NOT_APPLICABLE = "n/a"; static const char errorMessage[] = "Error"; #ifdef _WIN32 @@ -121,6 +122,15 @@ static bool ensureCapacity(PrintBuffer* buffer, size_t additional) { return true; } +static bool hasAnyLogicalLines(const RcnCountStatistics* stats) { + for (size_t i = 0; i < stats->count.size; ++i) { + if (stats->count.results[i].hasLogicalLines) { + return true; + } + } + return false; +} + /** * Puts a string up to `length` into the buffer. * The specified length must not exceed the actual length of `string`. @@ -233,6 +243,25 @@ static void prCnt(PrintBuffer* buffer, RcnCount value, int width) { prRpt(buffer, ' ', right); } +static void prNotApplicable(PrintBuffer* buffer) { + const size_t pad = ((WIDTH_COL1 - strlen(LABEL_NOT_APPLICABLE)) / 2) - 1; + prRpt(buffer, ' ', pad); + prStr(buffer, LABEL_NOT_APPLICABLE); + prRpt(buffer, ' ', pad); +} + +static void prLogicalLineCount( + PrintBuffer* buffer, + bool hasLogicalLines, + RcnCount value +) { + if (hasLogicalLines) { + prCnt(buffer, value, WIDTH_COL1); + } else { + prNotApplicable(buffer); + } +} + static void prHeaderCell(PrintBuffer* buffer, const char* label, int width) { assert(label != NULL); const int length = (int) strlen(label); @@ -384,7 +413,7 @@ static void prFileRowData( prChr(buffer, ' '); prChr(buffer, TABLE_BORDER_VERTICAL_NORMAL); prChr(buffer, ' '); - prCnt(buffer, res->logicalLines, WIDTH_COL1); + prLogicalLineCount(buffer, res->hasLogicalLines, res->logicalLines); prChr(buffer, ' '); prChr(buffer, TABLE_BORDER_VERTICAL_NORMAL); prChr(buffer, ' '); @@ -443,6 +472,7 @@ static void prSummaryRows( ) { for (RcnTextFormat frmt = 0; frmt < RECKON_NUM_SUPPORTED_FORMATS; ++frmt) { const char* label = NULL; + bool hasLogicalLines = false; switch (frmt) { case RCN_TEXT_UNFORMATTED: label = "Plain Text"; @@ -452,9 +482,11 @@ static void prSummaryRows( break; case RCN_LANG_C: label = "C"; + hasLogicalLines = true; break; case RCN_LANG_JAVA: label = "Java"; + hasLogicalLines = true; break; // LCOV_EXCL_START default: @@ -475,7 +507,7 @@ static void prSummaryRows( prChr(buffer, ' '); prChr(buffer, TABLE_BORDER_VERTICAL_NORMAL); prChr(buffer, ' '); - prCnt(buffer, stats->logicalLines[frmt], WIDTH_COL1); + prLogicalLineCount(buffer, hasLogicalLines, stats->logicalLines[frmt]); prChr(buffer, ' '); prChr(buffer, TABLE_BORDER_VERTICAL_NORMAL); prChr(buffer, ' '); @@ -506,7 +538,11 @@ static void prTotalsRow(PrintBuffer* buffer, const RcnCountStatistics* stats) { prChr(buffer, ' '); prChr(buffer, TABLE_BORDER_VERTICAL_NORMAL); prChr(buffer, ' '); - prCnt(buffer, stats->totalLogicalLines, WIDTH_COL1); + prLogicalLineCount( + buffer, + hasAnyLogicalLines(stats), + stats->totalLogicalLines + ); prChr(buffer, ' '); prChr(buffer, TABLE_BORDER_VERTICAL_NORMAL); prChr(buffer, ' '); @@ -540,7 +576,12 @@ PrintBuffer printResultSingle(const RcnCountStatistics* stats) { prChr(&buffer, '\n'); prChr(&buffer, '\n'); prStr(&buffer, " Logical Lines of Code (LLC): "); - pr8ld(&buffer, result->logicalLines); + if (result->hasLogicalLines) { + pr8ld(&buffer, result->logicalLines); + } else { + prRpt(&buffer, ' ', 8 - strlen(LABEL_NOT_APPLICABLE)); + prStr(&buffer, LABEL_NOT_APPLICABLE); + } prChr(&buffer, '\n'); prStr(&buffer, " Physical Lines (PHL): "); pr8ld(&buffer, result->physicalLines); diff --git a/src/scount/tests/functionality/res/expected/mixed.txt b/src/scount/tests/functionality/res/expected/mixed.txt index f094722..c418eec 100644 --- a/src/scount/tests/functionality/res/expected/mixed.txt +++ b/src/scount/tests/functionality/res/expected/mixed.txt @@ -5,18 +5,18 @@ Scanned files: 8 | Sample1.java | 3 | 12 | 34 | 233 | 233 | | Sample2.java | 4 | 13 | 38 | 286 | 286 | | sample1.c | 4 | 10 | 29 | 180 | 180 | - | sample1.md | 0 | 1 | 8 | 52 | 52 | - | sample1.txt | 0 | 1 | 9 | 52 | 52 | + | sample1.md | n/a | 1 | 8 | 52 | 52 | + | sample1.txt | n/a | 1 | 9 | 52 | 52 | | sample2.c | 5 | 11 | 33 | 219 | 219 | - | sample2.md | 0 | 1 | 8 | 53 | 53 | - | sample2.txt | 0 | 2 | 13 | 75 | 75 | + | sample2.md | n/a | 1 | 8 | 53 | 53 | + | sample2.txt | n/a | 2 | 13 | 75 | 75 | o--------------------------o-----------o-----------o-----------o-----------o-----------o Summary: o-------- Language --------o--- LLC ---o--- PHL ---o--- WRD ---o--- CHR ---o--- SZE ---o - | Plain Text | 0 | 3 | 22 | 127 | 127 | - | Markdown | 0 | 2 | 16 | 105 | 105 | + | Plain Text | n/a | 3 | 22 | 127 | 127 | + | Markdown | n/a | 2 | 16 | 105 | 105 | | C | 9 | 21 | 62 | 399 | 399 | | Java | 7 | 25 | 72 | 519 | 519 | o==========================o===========o===========o===========o===========o===========o diff --git a/src/scount/tests/functionality/res/expected/mixedWithSyntaxError.txt b/src/scount/tests/functionality/res/expected/mixedWithSyntaxError.txt index 34dcb42..72845db 100644 --- a/src/scount/tests/functionality/res/expected/mixedWithSyntaxError.txt +++ b/src/scount/tests/functionality/res/expected/mixedWithSyntaxError.txt @@ -3,13 +3,13 @@ Scanned files: 3 o---------- File ----------o--- LLC ---o--- PHL ---o--- WRD ---o--- CHR ---o--- SZE ---o | CorrectFile.java | 5 | 12 | 33 | 273 | 273 | - | correct_file.txt | 0 | 1 | 20 | 109 | 109 | + | correct_file.txt | n/a | 1 | 20 | 109 | 109 | o--------------------------o-----------o-----------o-----------o-----------o-----------o Summary: o-------- Language --------o--- LLC ---o--- PHL ---o--- WRD ---o--- CHR ---o--- SZE ---o - | Plain Text | 0 | 1 | 20 | 109 | 109 | + | Plain Text | n/a | 1 | 20 | 109 | 109 | | Java | 5 | 12 | 33 | 273 | 273 | o==========================o===========o===========o===========o===========o===========o | Total: | 5 | 13 | 53 | 382 | 382 | diff --git a/src/scount/tests/functionality/res/expected/output_multiple_files_no_llc.txt b/src/scount/tests/functionality/res/expected/output_multiple_files_no_llc.txt new file mode 100644 index 0000000..a62abe9 --- /dev/null +++ b/src/scount/tests/functionality/res/expected/output_multiple_files_no_llc.txt @@ -0,0 +1,21 @@ +Directory: txt +Scanned files: 6 + + o---------- File ----------o--- LLC ---o--- PHL ---o--- WRD ---o--- CHR ---o--- SZE ---o + | 1sample1.txt | n/a | 1 | 2 | 26 | 26 | + | 1sample2.txt | n/a | 1 | 2 | 26 | 26 | + | 1sample3.txt | n/a | 1 | 2 | 26 | 26 | + | 2sample1.txt | n/a | 1 | 2 | 31 | 31 | + | 2sample2.txt | n/a | 1 | 2 | 31 | 31 | + | 3sample1.txt | n/a | 1 | 2 | 36 | 36 | + o--------------------------o-----------o-----------o-----------o-----------o-----------o + +Summary: + + o-------- Language --------o--- LLC ---o--- PHL ---o--- WRD ---o--- CHR ---o--- SZE ---o + | Plain Text | n/a | 6 | 12 | 176 | 176 | + o==========================o===========o===========o===========o===========o===========o + | Total: | n/a | 6 | 12 | 176 | 176 | + o==========================o===========o===========o===========o===========o===========o + + diff --git a/src/scount/tests/functionality/res/expected/output_single_file_no_llc.txt b/src/scount/tests/functionality/res/expected/output_single_file_no_llc.txt new file mode 100644 index 0000000..d882727 --- /dev/null +++ b/src/scount/tests/functionality/res/expected/output_single_file_no_llc.txt @@ -0,0 +1,8 @@ +File: sample1.md + + Logical Lines of Code (LLC): n/a + Physical Lines (PHL): 1 + Words (WRD): 8 + Characters (CHR): 52 + Source Size in Bytes (SZE): 52 + diff --git a/src/scount/tests/functionality/sh/test_scount.sh b/src/scount/tests/functionality/sh/test_scount.sh index a85a0a3..2dac77a 100644 --- a/src/scount/tests/functionality/sh/test_scount.sh +++ b/src/scount/tests/functionality/sh/test_scount.sh @@ -45,6 +45,20 @@ function test_scount_with_relative_directory_input() { assert_stderr_is_empty; } +function test_scount_prints_correct_output_for_single_file_no_llc() { + run_app "${TEST_RES_DIR}/mixed/sample1.md"; + assert_exit_status $EXIT_SUCCESS; + assert_stdout_equals_file "expected/output_single_file_no_llc.txt"; + assert_stderr_is_empty; +} + +function test_scount_prints_correct_output_for_multiple_files_no_llc() { + run_app "${TEST_PROJECT_DIR}/src/lib/tests/res/txt"; + assert_exit_status $EXIT_SUCCESS; + assert_stdout_equals_file "expected/output_multiple_files_no_llc.txt"; + assert_stderr_is_empty; +} + function test_scount_prints_annotated_source_code() { run_app --annotate-counts "${TEST_PROJECT_DIR}/src/lib/tests/res/java/Sample.java"; assert_exit_status $EXIT_SUCCESS; diff --git a/src/scount/tests/unit/c/test_print.c b/src/scount/tests/unit/c/test_print.c index 063635f..ce07efe 100644 --- a/src/scount/tests/unit/c/test_print.c +++ b/src/scount/tests/unit/c/test_print.c @@ -15,6 +15,7 @@ */ #include +#include #include #include @@ -50,6 +51,7 @@ static RcnCountStatistics* mkStats( stats->count.results[i].words = words; stats->count.results[i].characters = characters; stats->count.results[i].sourceSize = sourceSize; + stats->count.results[i].hasLogicalLines = true; } stats->logicalLines[RCN_LANG_JAVA] = logical; stats->physicalLines[RCN_LANG_JAVA] = physical;