@sasjs/lint 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,8 @@ Configuration is via a `.sasjslint` file with the following structure (these are
59
59
 
60
60
  Two keys do not have a section of their own. `requiredMacroOptions` is documented with [`hasRequiredMacroOptions`](#hasrequiredmacrooptions), the rule it configures. The deprecated alias `noTabIndentation` is accepted in place of `noTabs`, and is therefore not listed above - it is an alternative spelling rather than a setting of its own.
61
61
 
62
+ The file is read as strict JSON, so it cannot carry comments: a file that does not parse is reported as an error, naming the file, rather than being treated as though no configuration were present. Add `"$schema": "https://cli.sasjs.io/sasjslint-schema.json"` to get completion, hover text and validation for every setting in an editor that understands JSON schemas - `sasjs lint init` and a SASjs Server's seeded drive-root file both write it.
63
+
62
64
  ## SAS Lint Settings
63
65
 
64
66
  Each setting can have three states:
@@ -331,6 +333,46 @@ In batch mode, long SAS code lines may also be truncated, causing hard-to-detect
331
333
 
332
334
  We strongly recommend a line length limit, and set the bar at 80. To turn this feature off, set the value to 0.
333
335
 
336
+ Saving with the formatter does four things for this rule, in this order.
337
+
338
+ **It lifts a trailing comment onto its own line** when a line is over the limit and carries code before the comment. The code keeps the line it was on and the comment moves below it at the same indentation, which hands the code to the wrap below and the prose to the reflow - so `, var /* variable name from which to return the format */` becomes two lines, and each is handled by the pass that owns it. The lift happens even when the code alone overruns the limit: each half then reaches the pass that owns it, instead of the line being refused by both.
339
+
340
+ **It breaks a long line of code** at the last space that still fits, and indents the continuation by the next multiple of [`indentationMultiple`](#indentationmultiple), so a wrap never introduces an indentation warning of its own. A space that follows a `;` is preferred over one inside a statement: the semicolon ends the statement, so a newline there is inert and the line reads as SAS rather than as a slice of one, and for a `put` or `%let` it needs none of the `%trim(` machinery below. It also prefers a space that does not leave a macro keyword such as `%let` or `%then` at the end of the line, and a line is rewritten only when every line it becomes fits the limit - so the pass never trades one warning for another, which also keeps `fixedDiagnosticsCount` from going backwards. The shapes where a break would change what SAS reads, or what another rule parses, are left alone:
341
+
342
+ - `datalines` / `cards` / `parmcards` records, and the statement that opens them, where a newline ends the record
343
+ - a `%*` comment statement, which is prose
344
+ - any space inside `%str`-family masking or a `%sysfunc` / `%qsysfunc` call, whose argument text is carried as data: measured on a live engine, breaking inside `%sysfunc(fput(...))` wrote `Content-Type: application/csv` where the macro meant one space
345
+
346
+ A `put`, `putlog` or `%put` statement (including the `%&var.put` form), the window statements, and a `%let` assignment are broken differently, because a plain newline there is data rather than a delimiter: it is a line of the log for the `put` family, and part of the value for `%let`. The break ends the line with `%trim(` and opens the next with `)`, so the newline and the indentation become the argument of `%trim` - which resolves to nothing - and the blank the break replaced sits after the closing marker:
347
+
348
+ ```sas
349
+ %put NOTE: the macro could not find the requested%trim(
350
+ ) table and stopped;
351
+ %let exp8=MPE_DERIVED:CALLER:VAR_LOGIC may not call%trim(
352
+ ) a function that;
353
+ ```
354
+
355
+ Leaving the blank at the end of the upper line instead would lose it: measured on a live engine, `requested %trim(` printed `requestedtable`, because SAS drops a blank that sits immediately before a macro call - and a trailing blank is what `noTrailingSpaces` reports anyway. The construct is verified end to end for both forms: a macro holding these statements writes byte-identical log lines, and the `%let` variables hold byte-identical values, before and after a format at a limit of 40 and at 60, several breaks per statement included. A `%let` value built over two statements, and one whose text already carries a `%trim(` of its own, are covered by the same check.
356
+
357
+ A `%macro` declaration is broken before its parameter commas, and the continuation line starts with the comma, so the parameters read as a list and the declaration stays the one statement the macro rules parse:
358
+
359
+ ```sas
360
+ %macro mf_writefile(fpath
361
+ ,mode=O,l1=,l2=,l3=,l4=,l5=,l6=,l7=,l8=,l9=,l10=);
362
+ ```
363
+
364
+ **It reflows the prose in a block comment**, paragraph by paragraph: the lines of one paragraph are joined and rewrapped, so the block comes back even rather than ragged. A paragraph breaks at a blank line and before anything line-oriented - an `@li` entry, a `@sasjslint` tag, an `<h4>` heading, a list item, a table row or a macro trigger - and a `@brief` or `@param` line carries the lines under it. Blocks holding a `@example` / `@code` / `@verbatim` body, the `<h4> SAS Macros </h4>` section, a comment that shares a line with code, and a comment inside a `datalines` record are left byte-identical. Prose written with a `*` at the start of every line - the JavaDoc style - is read as a list and left as it is. A reflow normalises the runs of spaces inside the paragraph it rewrites, so two spaces after a full stop become one.
365
+
366
+ **It raises the file's own exception** for a line neither pass could shorten - a URL in a header, a long quoted literal that generates code, an indented data record - by writing or merging a [`@sasjslint`](#per-file-overrides) block at the end of the header:
367
+
368
+ ```sas
369
+ @sasjslint {"maxHeaderLineLength": 123}
370
+ ```
371
+
372
+ The value is the exact length the longest such line needs and no more: a header line raises `maxHeaderLineLength`, a data record raises `maxDataLineLength`, and a body line raises `maxLineLength`. An existing exception keeps every key it had, and a value is never lowered.
373
+
374
+ Nothing is raised for a line whose author can still act on it: a `%*` comment, a `put` whose text holds a word longer than the limit, a `%macro` declaration whose single parameter is too long, or a comment line that a reflow cannot fit. Excusing one of those would relax `maxLineLength` for every line of code in the file, and each is a line the author can rewrite.
375
+
334
376
  - Default: 80
335
377
  - Severity: WARNING
336
378
 
@@ -688,6 +730,9 @@ We've already implemented the following rules:
688
730
  - Remove unused macros from the `<h4> SAS Macros </h4>` section
689
731
  - Remove trailing spaces
690
732
  - Fix line endings, including adding a missing final newline when [`lineEndings`](#lineendings) is configured
733
+ - Break a line that exceeds [`maxLineLength`](#maxlinelength) at whitespace, where doing so cannot change what SAS reads
734
+ - Reflow the prose in a block comment, paragraph by paragraph
735
+ - Raise the file's own `@sasjslint` exception for a line neither of the above can shorten
691
736
 
692
737
  The formatter only fixes rules that are in your configuration, so a rule that is off neither warns nor is repaired.
693
738
 
@@ -0,0 +1,34 @@
1
+ import { LintConfig } from '../types';
2
+ /**
3
+ * Raises the file's own `@sasjslint` exception so that the lines the passes
4
+ * could not shorten stop being reported.
5
+ *
6
+ * The formatter can break most long lines, and `reflowComments` can reflow
7
+ * prose, but a single token wider than the limit - a URL in a header, a long
8
+ * quoted literal that generates code - cannot be shortened at all, and a data
9
+ * record is not code. The documented way to accept those is the per-file
10
+ * override, and projects do it by hand today (sasjs/core carries
11
+ * `{"maxLineLength": 123}` for one documentation URL). This writes it for them,
12
+ * at the exact length the longest such line needs and no more:
13
+ *
14
+ * - a header line raises `maxHeaderLineLength`, which affects header lines only
15
+ * - a data record raises `maxDataLineLength`, which affects records only
16
+ * - a body line raises `maxLineLength`, which affects the whole file
17
+ *
18
+ * Lines the wrapper declines on purpose - `put`-family statements, where a
19
+ * break is an extra line of output, and `%macro` / `%mend` declarations - do NOT
20
+ * raise the limit: they are fixable by hand, and excusing them would hide the
21
+ * finding for good.
22
+ *
23
+ * The value is never lowered, an existing exception keeps every key it had, and
24
+ * no header means nowhere to write, in which case the text is returned
25
+ * untouched.
26
+ *
27
+ * @param {string} text - the text to adjust.
28
+ * @param {LintConfig} configuration - the effective configuration, the file's
29
+ * own override included.
30
+ * @returns {string} the text, with its exception raised when one is needed.
31
+ */
32
+ export declare const adjustLineLength: (text: string, configuration: LintConfig) => string;
33
+ /** The configured line ending, for the rare case a terminator has to be invented. */
34
+ export declare const lineEndingOf: (configuration: LintConfig) => string;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lineEndingOf = exports.adjustLineLength = void 0;
4
+ const LineEndings_1 = require("../types/LineEndings");
5
+ const getDataSectionsDetail_1 = require("../utils/getDataSectionsDetail");
6
+ const getHeaderLinesCount_1 = require("../utils/getHeaderLinesCount");
7
+ const parseLintOverride_1 = require("../utils/parseLintOverride");
8
+ const scanLine_1 = require("../utils/scanLine");
9
+ const longLines_1 = require("./longLines");
10
+ const rewrapText_1 = require("./rewrapText");
11
+ /** The line that carries a file's own lint configuration. */
12
+ const overrideTag = /^(\s*)@sasjslint\s*(\{.*\})\s*$/;
13
+ /**
14
+ * Raises the file's own `@sasjslint` exception so that the lines the passes
15
+ * could not shorten stop being reported.
16
+ *
17
+ * The formatter can break most long lines, and `reflowComments` can reflow
18
+ * prose, but a single token wider than the limit - a URL in a header, a long
19
+ * quoted literal that generates code - cannot be shortened at all, and a data
20
+ * record is not code. The documented way to accept those is the per-file
21
+ * override, and projects do it by hand today (sasjs/core carries
22
+ * `{"maxLineLength": 123}` for one documentation URL). This writes it for them,
23
+ * at the exact length the longest such line needs and no more:
24
+ *
25
+ * - a header line raises `maxHeaderLineLength`, which affects header lines only
26
+ * - a data record raises `maxDataLineLength`, which affects records only
27
+ * - a body line raises `maxLineLength`, which affects the whole file
28
+ *
29
+ * Lines the wrapper declines on purpose - `put`-family statements, where a
30
+ * break is an extra line of output, and `%macro` / `%mend` declarations - do NOT
31
+ * raise the limit: they are fixable by hand, and excusing them would hide the
32
+ * finding for good.
33
+ *
34
+ * The value is never lowered, an existing exception keeps every key it had, and
35
+ * no header means nowhere to write, in which case the text is returned
36
+ * untouched.
37
+ *
38
+ * @param {string} text - the text to adjust.
39
+ * @param {LintConfig} configuration - the effective configuration, the file's
40
+ * own override included.
41
+ * @returns {string} the text, with its exception raised when one is needed.
42
+ */
43
+ const adjustLineLength = (text, configuration) => {
44
+ if (!text)
45
+ return text;
46
+ if (!configuration.lineLintRules.some((rule) => rule.name === 'maxLineLength'))
47
+ return text;
48
+ const headerLinesCount = (0, getHeaderLinesCount_1.getHeaderLinesCount)(text, configuration);
49
+ if (!headerLinesCount)
50
+ return text;
51
+ const entries = (0, rewrapText_1.splitWithTerminators)(text);
52
+ const lines = entries.map((entry) => entry.line);
53
+ const dataSections = (0, getDataSectionsDetail_1.getDataSectionsDetail)(text, configuration);
54
+ const headerLimit = Math.max(configuration.maxLineLength, configuration.maxHeaderLineLength);
55
+ const dataLimit = Math.max(configuration.maxLineLength, configuration.maxDataLineLength);
56
+ const needed = {
57
+ maxLineLength: 0,
58
+ maxHeaderLineLength: 0,
59
+ maxDataLineLength: 0
60
+ };
61
+ let state = (0, scanLine_1.cleanState)();
62
+ let breakAwareOpen = false;
63
+ lines.forEach((line, index) => {
64
+ const isHeader = index < headerLinesCount;
65
+ const isData = (0, getDataSectionsDetail_1.checkIsDataLine)(dataSections, index);
66
+ // A data record is data, so it is not scanned: a comment marker or an
67
+ // apostrophe in one would otherwise stay open for the rest of the file, and
68
+ // every later line would read as a comment that no pass may touch.
69
+ const scanned = isData ? (0, scanLine_1.noScan)() : (0, scanLine_1.scanLine)(line, state);
70
+ state = scanned.endState;
71
+ const breakAware = isData
72
+ ? { line: false, open: false }
73
+ : (0, longLines_1.breakAwareState)(scanned.code, breakAwareOpen);
74
+ breakAwareOpen = breakAware.open;
75
+ const limit = isData
76
+ ? dataLimit
77
+ : isHeader
78
+ ? headerLimit
79
+ : configuration.maxLineLength;
80
+ if (line.length <= limit)
81
+ return;
82
+ if (breakAware.line ||
83
+ longLines_1.macroComment.test(line) ||
84
+ longLines_1.macroDeclaration.test(line))
85
+ return;
86
+ // A comment carries no key of its own, so excusing one would raise the
87
+ // limit for every line of code in the file: `reflowComments` has already
88
+ // reflowed the prose, and a paragraph holding a word it cannot fit - a URL -
89
+ // is left to its author. A header line is the exception, because
90
+ // `maxHeaderLineLength` scopes the relaxation to the header.
91
+ //
92
+ // The guard turns on what makes the line long, not on the comment being
93
+ // there: a line of code that overruns the limit on its own, with a comment
94
+ // after it, is excused like any other, because no pass may break it either.
95
+ const codeEnd = scanned.code.trimEnd().length;
96
+ if (!isHeader && !isData && scanned.hasComment && codeEnd <= limit)
97
+ return;
98
+ const key = isData
99
+ ? 'maxDataLineLength'
100
+ : isHeader
101
+ ? 'maxHeaderLineLength'
102
+ : 'maxLineLength';
103
+ needed[key] = Math.max(needed[key], line.length);
104
+ });
105
+ const raised = Object.keys(needed).filter((key) => needed[key] > configuration[key]);
106
+ if (!raised.length)
107
+ return text;
108
+ const override = (0, parseLintOverride_1.getLintOverride)(text, configuration);
109
+ const existing = lines
110
+ .slice(0, headerLinesCount)
111
+ .find((line) => overrideTag.test(line));
112
+ // A tag that does not parse is reported by the linter and belongs to its
113
+ // author: rewriting it would discard whatever they were trying to say.
114
+ if (existing && !override)
115
+ return text;
116
+ const merged = Object.assign({}, (override !== null && override !== void 0 ? override : {}));
117
+ raised.forEach((key) => {
118
+ var _a;
119
+ merged[key] = Math.max((_a = merged[key]) !== null && _a !== void 0 ? _a : 0, needed[key]);
120
+ });
121
+ const json = '{' +
122
+ Object.keys(merged)
123
+ .map((key) => `"${key}": ${JSON.stringify(merged[key])}`)
124
+ .join(', ') +
125
+ '}';
126
+ if (existing) {
127
+ const match = overrideTag.exec(existing);
128
+ if (match[2] === json)
129
+ return text;
130
+ const at = lines.indexOf(existing);
131
+ const rewritten = [...lines];
132
+ rewritten[at] = `${match[1]}@sasjslint ${json}`;
133
+ return rewritten.map((line, index) => line + entries[index].ending).join('');
134
+ }
135
+ // No exception yet: write one as the last content line of the header, which
136
+ // is where sasjs/core keeps them, so the header's own layout is untouched.
137
+ const closingIndex = headerLinesCount - 1;
138
+ const closing = lines[closingIndex];
139
+ if (!/^\s*[*]{2}\/\s*$/.test(closing))
140
+ return text;
141
+ const previous = lines[closingIndex - 1];
142
+ const indent = /^\s*/.exec(previous)[0] || ' ';
143
+ const ending = entries[closingIndex].ending || (0, exports.lineEndingOf)(configuration);
144
+ const separator = previous.trim() === '' ? [] : [''];
145
+ const inserted = [...separator, `${indent}@sasjslint ${json}`];
146
+ const out = lines
147
+ .slice(0, closingIndex)
148
+ .map((line, index) => line + entries[index].ending);
149
+ inserted.forEach((line) => out.push(line + ending));
150
+ lines.slice(closingIndex).forEach((line, index) => {
151
+ out.push(line + entries[closingIndex + index].ending);
152
+ });
153
+ return out.join('');
154
+ };
155
+ exports.adjustLineLength = adjustLineLength;
156
+ /** The configured line ending, for the rare case a terminator has to be invented. */
157
+ const lineEndingOf = (configuration) => configuration.lineEndings === LineEndings_1.LineEndings.CRLF ? '\r\n' : '\n';
158
+ exports.lineEndingOf = lineEndingOf;
159
+ //# sourceMappingURL=adjustLineLength.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adjustLineLength.js","sourceRoot":"","sources":["../../src/format/adjustLineLength.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { LintConfig } from '../types';
2
+ /** A macro declaration, which the macro rules read as one statement. */
3
+ export declare const macroDeclaration: RegExp;
4
+ /**
5
+ * A macro comment statement - `%* text ;` - which is prose that happens to start
6
+ * with a trigger. Breaking it would put the terminator on a line of its own, and
7
+ * like a block comment it belongs to its author.
8
+ */
9
+ export declare const macroComment: RegExp;
10
+ /**
11
+ * A statement whose line breaks are data rather than space between tokens.
12
+ *
13
+ * For the DATA step's `put` and `putlog`, and the macro and window statements
14
+ * that write text a reader sees, a break adds a line to the log or to the file
15
+ * being written. For `%let` a break lands inside the value, and the value keeps
16
+ * the newline and the continuation indent - measured on a live engine, where a
17
+ * wrapped `%let` message compared against a single-spaced one failed.
18
+ *
19
+ * Both are broken with the `%trim(` idiom rather than by a plain newline, and
20
+ * neither is left as it is: a break that carries `%trim(` adds no whitespace and
21
+ * no output line.
22
+ */
23
+ export declare const breakAwareStatement: RegExp;
24
+ /**
25
+ * The index where the text of a `put`-family statement begins, so a break is
26
+ * never taken before the keyword that names it.
27
+ *
28
+ * A continuation line carries no keyword of its own, and its whole content is
29
+ * text, so there the text begins at the first non-whitespace character.
30
+ *
31
+ * @param {string} line - the line to read.
32
+ * @returns {number} the index of the first character of the statement's text.
33
+ */
34
+ export declare const putTextStart: (line: string) => number;
35
+ /** The index of the first non-whitespace character at or after `from`. */
36
+ export declare const firstNonSpace: (line: string, from: number) => number;
37
+ /**
38
+ * The indentation a continuation line takes: the next multiple of
39
+ * `indentationMultiple` after the line's own, so a break can never introduce an
40
+ * `indentationMultiple` diagnostic of its own.
41
+ */
42
+ export declare const continuationIndentFor: (indent: string, multiple: number) => string;
43
+ /** The indentation multiple in force, with the rule's documented default. */
44
+ export declare const indentationMultipleOf: (configuration: LintConfig) => number;
45
+ /**
46
+ * Reads whether one line opens, or carries the continuation of, a statement
47
+ * whose line breaks SAS reads as output lines.
48
+ *
49
+ * Splitting the code view on `;` gives one entry per statement on the line, the
50
+ * last entry being the part that has not reached its own terminator.
51
+ *
52
+ * @param {string} code - the line with literals and comments blanked.
53
+ * @param {boolean} open - whether the previous line left such a statement open.
54
+ * @returns {{ line: boolean, open: boolean }} whether this line belongs to one,
55
+ * and whether the next line still does.
56
+ */
57
+ export declare const breakAwareState: (code: string, open: boolean) => {
58
+ line: boolean;
59
+ open: boolean;
60
+ };
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.breakAwareState = exports.indentationMultipleOf = exports.continuationIndentFor = exports.firstNonSpace = exports.putTextStart = exports.breakAwareStatement = exports.macroComment = exports.macroDeclaration = void 0;
4
+ /** A macro declaration, which the macro rules read as one statement. */
5
+ exports.macroDeclaration = /^\s*%(?:macro|mend)\b/i;
6
+ /**
7
+ * A macro comment statement - `%* text ;` - which is prose that happens to start
8
+ * with a trigger. Breaking it would put the terminator on a line of its own, and
9
+ * like a block comment it belongs to its author.
10
+ */
11
+ exports.macroComment = /^\s*%\*/;
12
+ /**
13
+ * A statement whose line breaks are data rather than space between tokens.
14
+ *
15
+ * For the DATA step's `put` and `putlog`, and the macro and window statements
16
+ * that write text a reader sees, a break adds a line to the log or to the file
17
+ * being written. For `%let` a break lands inside the value, and the value keeps
18
+ * the newline and the continuation indent - measured on a live engine, where a
19
+ * wrapped `%let` message compared against a single-spaced one failed.
20
+ *
21
+ * Both are broken with the `%trim(` idiom rather than by a plain newline, and
22
+ * neither is left as it is: a break that carries `%trim(` adds no whitespace and
23
+ * no output line.
24
+ */
25
+ exports.breakAwareStatement = /^\s*%?(?:&?[\w.]*\.)?(?:put|putlog|window|display)\b|\b(?:then|else)\s+%?put\b|^\s*%let\b/i;
26
+ /**
27
+ * The index where the text of a `put`-family statement begins, so a break is
28
+ * never taken before the keyword that names it.
29
+ *
30
+ * A continuation line carries no keyword of its own, and its whole content is
31
+ * text, so there the text begins at the first non-whitespace character.
32
+ *
33
+ * @param {string} line - the line to read.
34
+ * @returns {number} the index of the first character of the statement's text.
35
+ */
36
+ const putTextStart = (line) => {
37
+ const match = exports.breakAwareStatement.exec(line);
38
+ if (!match)
39
+ return (0, exports.firstNonSpace)(line, 0);
40
+ const afterKeyword = match.index + match[0].length;
41
+ // A `%let` keeps its name out of the value, so the first break belongs after
42
+ // the `=` rather than between the keyword and the name.
43
+ if (/^\s*%let\b/i.test(line)) {
44
+ const equals = line.indexOf('=', afterKeyword);
45
+ if (equals > -1)
46
+ return equals + 1;
47
+ }
48
+ return afterKeyword;
49
+ };
50
+ exports.putTextStart = putTextStart;
51
+ /** The index of the first non-whitespace character at or after `from`. */
52
+ const firstNonSpace = (line, from) => {
53
+ const rest = line.slice(from);
54
+ return from + rest.length - rest.trimStart().length;
55
+ };
56
+ exports.firstNonSpace = firstNonSpace;
57
+ /**
58
+ * The indentation a continuation line takes: the next multiple of
59
+ * `indentationMultiple` after the line's own, so a break can never introduce an
60
+ * `indentationMultiple` diagnostic of its own.
61
+ */
62
+ const continuationIndentFor = (indent, multiple) => indent + ' '.repeat(multiple - (indent.length % multiple));
63
+ exports.continuationIndentFor = continuationIndentFor;
64
+ /** The indentation multiple in force, with the rule's documented default. */
65
+ const indentationMultipleOf = (configuration) => configuration.indentationMultiple > 0 ? configuration.indentationMultiple : 2;
66
+ exports.indentationMultipleOf = indentationMultipleOf;
67
+ /**
68
+ * Reads whether one line opens, or carries the continuation of, a statement
69
+ * whose line breaks SAS reads as output lines.
70
+ *
71
+ * Splitting the code view on `;` gives one entry per statement on the line, the
72
+ * last entry being the part that has not reached its own terminator.
73
+ *
74
+ * @param {string} code - the line with literals and comments blanked.
75
+ * @param {boolean} open - whether the previous line left such a statement open.
76
+ * @returns {{ line: boolean, open: boolean }} whether this line belongs to one,
77
+ * and whether the next line still does.
78
+ */
79
+ const breakAwareState = (code, open) => {
80
+ const statements = code.split(';');
81
+ const at = statements.findIndex((statement) => exports.breakAwareStatement.test(statement));
82
+ return {
83
+ line: open || at > -1,
84
+ open: open
85
+ ? statements.length === 1
86
+ : at > -1 && at === statements.length - 1
87
+ };
88
+ };
89
+ exports.breakAwareState = breakAwareState;
90
+ //# sourceMappingURL=longLines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"longLines.js","sourceRoot":"","sources":["../../src/format/longLines.ts"],"names":[],"mappings":";;;AAEA,wEAAwE;AAC3D,QAAA,gBAAgB,GAAG,wBAAwB,CAAA;AAExD;;;;GAIG;AACU,QAAA,YAAY,GAAG,SAAS,CAAA;AAErC;;;;;;;;;;;;GAYG;AACU,QAAA,mBAAmB,GAC9B,4FAA4F,CAAA;AAE9F;;;;;;;;;GASG;AACI,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE;IACnD,MAAM,KAAK,GAAG,2BAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE5C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAA,qBAAa,EAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAEzC,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAElD,6EAA6E;IAC7E,wDAAwD;IACxD,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;QAE9C,IAAI,MAAM,GAAG,CAAC,CAAC;YAAE,OAAO,MAAM,GAAG,CAAC,CAAA;IACpC,CAAC;IAED,OAAO,YAAY,CAAA;AACrB,CAAC,CAAA;AAhBY,QAAA,YAAY,gBAgBxB;AAED,0EAA0E;AACnE,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,IAAY,EAAU,EAAE;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAE7B,OAAO,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,MAAM,CAAA;AACrD,CAAC,CAAA;AAJY,QAAA,aAAa,iBAIzB;AAED;;;;GAIG;AACI,MAAM,qBAAqB,GAAG,CACnC,MAAc,EACd,QAAgB,EACR,EAAE,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAA;AAH1D,QAAA,qBAAqB,yBAGqC;AAEvE,6EAA6E;AACtE,MAAM,qBAAqB,GAAG,CAAC,aAAyB,EAAU,EAAE,CACzE,aAAa,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;AADlE,QAAA,qBAAqB,yBAC6C;AAE/E;;;;;;;;;;;GAWG;AACI,MAAM,eAAe,GAAG,CAC7B,IAAY,EACZ,IAAa,EACqB,EAAE;IACpC,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAClC,MAAM,EAAE,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,EAAE,CAC5C,2BAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CACpC,CAAA;IAED,OAAO;QACL,IAAI,EAAE,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;QACrB,IAAI,EAAE,IAAI;YACR,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;YACzB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,UAAU,CAAC,MAAM,GAAG,CAAC;KAC5C,CAAA;AACH,CAAC,CAAA;AAfY,QAAA,eAAe,mBAe3B"}
@@ -0,0 +1,28 @@
1
+ import { LintConfig } from '../types';
2
+ /**
3
+ * Reflows the prose inside a file's block comments so that no line exceeds the
4
+ * limit that applies to it, and leaves every other line byte-identical.
5
+ *
6
+ * A paragraph is a run of comment lines at one indentation that carry prose. It
7
+ * breaks at a blank line and before anything line-oriented: an `@li` entry, a
8
+ * `@sasjslint` tag, an HTML heading, a list marker, a table row or a macro
9
+ * trigger. Wrapping one of those - or wrapping one line of a paragraph without
10
+ * reflowing the rest - is what leaves a header ragged. A `@brief` or `@param`
11
+ * line is prose, so it carries the lines that follow it.
12
+ *
13
+ * Blocks that are left entirely alone: one sharing a line with code, one inside
14
+ * a `datalines` record, one holding a `@example` / `@code` / `@verbatim` body,
15
+ * the `<h4> SAS Macros </h4>` section - whose entries other rules read and
16
+ * rewrite - and any block holding a `@sasjslint` tag, whose object has to stay
17
+ * on the line the tag opens.
18
+ *
19
+ * A paragraph is reflowed only when every line of the result fits, so the pass
20
+ * never raises the diagnostic count: a word longer than the limit - a URL,
21
+ * usually - leaves its paragraph as the author wrote it.
22
+ *
23
+ * @param {string} text - the text to reflow.
24
+ * @param {LintConfig} configuration - the configuration in force, header
25
+ * override included.
26
+ * @returns {string} the reflowed text.
27
+ */
28
+ export declare const reflowComments: (text: string, configuration: LintConfig) => string;