basalt-ui 1.21.0 → 1.23.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/AGENTS.md +11 -1
- package/CHANGELOG.md +33 -0
- package/MIGRATING.md +205 -9
- package/README.md +63 -16
- package/agent/rules/basalt-app.md +39 -9
- package/agent/rules/basalt-charts.md +41 -6
- package/agent/rules/basalt-router.md +1 -1
- package/agent/rules/basalt-state.md +4 -4
- package/agent/rules/basalt-tokens.md +25 -4
- package/agent/skills/basalt-app/SKILL.md +4 -3
- package/agent/skills/basalt-charts/SKILL.md +23 -8
- package/agent/templates/DESIGN.md.tpl +3 -1
- package/configs/oxlint-plugin.js +86 -9
- package/configs/oxlint-plugin.test.ts +173 -0
- package/dist/charts/hooks/useBandPlot.d.ts +138 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
- package/dist/charts/hooks/useBandPlot.js +127 -0
- package/dist/charts/index.d.ts +4 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +14 -0
- package/dist/charts/kinds/BandStrip.d.ts +113 -0
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
- package/dist/charts/kinds/BandStrip.js +218 -0
- package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
- package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
- package/dist/charts/kinds/Bars.d.ts +5 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +2 -0
- package/dist/charts/kinds/MirroredBars.d.ts +93 -0
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
- package/dist/charts/kinds/MirroredBars.js +315 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
- package/dist/charts/kinds/MultiLine.d.ts +5 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +2 -0
- package/dist/charts/kinds/StackedArea.d.ts +5 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +2 -0
- package/dist/charts/kinds/ZonedLine.d.ts +5 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +2 -0
- package/dist/charts/primitives/CartesianChart.d.ts +18 -2
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/HatchPattern.d.ts +31 -0
- package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
- package/dist/charts/primitives/HatchPattern.js +31 -0
- package/dist/cli/index.d.ts +9 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +275 -48
- package/dist/cli/shipped-versions.test.d.ts +2 -0
- package/dist/cli/shipped-versions.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +67 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +70 -11
- package/dist/vite.d.ts +44 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +71 -26
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/hooks/useBandPlot.ts +302 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/kinds/BandStrip.test.tsx +200 -0
- package/src/charts/kinds/BandStrip.tsx +364 -0
- package/src/charts/kinds/Bars.tsx +7 -1
- package/src/charts/kinds/DualPanel.test.tsx +10 -4
- package/src/charts/kinds/MirroredBars.test.tsx +193 -0
- package/src/charts/kinds/MirroredBars.tsx +455 -0
- package/src/charts/kinds/MultiLine.tsx +7 -1
- package/src/charts/kinds/StackedArea.tsx +7 -1
- package/src/charts/kinds/ZonedLine.tsx +7 -1
- package/src/charts/primitives/CartesianChart.test.tsx +86 -4
- package/src/charts/primitives/CartesianChart.tsx +28 -3
- package/src/charts/primitives/HatchPattern.tsx +55 -0
- package/src/cli/check-theme.test.ts +35 -0
- package/src/cli/help.test.ts +5 -1
- package/src/cli/index.ts +552 -79
- package/src/cli/shipped-versions.test.ts +83 -0
- package/src/cli/toolchain-wiring.test.ts +362 -6
- package/src/cli/waiver-audit.test.ts +88 -4
- package/src/guard/check-source.test.ts +293 -0
- package/src/guard/index.ts +209 -20
- package/src/vite.test.ts +112 -0
- package/src/vite.ts +147 -31
|
@@ -13,10 +13,14 @@ import { SPACE_SCALE } from '../tokens/palette'
|
|
|
13
13
|
import {
|
|
14
14
|
checkSource,
|
|
15
15
|
DEFAULT_GUARD_CONFIG,
|
|
16
|
+
findAllowAnnotations,
|
|
16
17
|
GENERATED_HEADER_LINE,
|
|
17
18
|
guardKindRemedy,
|
|
18
19
|
guardWaiverHint,
|
|
19
20
|
GUARD_RULES,
|
|
21
|
+
neutralizeAllowAnnotation,
|
|
22
|
+
NEUTRALIZED_ALLOW_TOKEN,
|
|
23
|
+
PLUGIN_RULE_IDS,
|
|
20
24
|
TOKENS_ONLY_DISABLED_KINDS,
|
|
21
25
|
unmatchedExemptPatterns,
|
|
22
26
|
} from './index'
|
|
@@ -2733,3 +2737,292 @@ describe('inline-spacing — the style-object test', () => {
|
|
|
2733
2737
|
expect(kinds(f)).toContain('inline-spacing')
|
|
2734
2738
|
})
|
|
2735
2739
|
})
|
|
2740
|
+
|
|
2741
|
+
// ── theme-allow shape grid ───────────────────────────────────────────────────────────────────────
|
|
2742
|
+
|
|
2743
|
+
/**
|
|
2744
|
+
* The whole shape space of an annotation, pinned cell by cell.
|
|
2745
|
+
*
|
|
2746
|
+
* FIVE holes have been found in this one contract in four rounds — two false positives, one false
|
|
2747
|
+
* negative, and twice the guard and the oxlint plugin disagreeing about what an annotation IS. Each
|
|
2748
|
+
* was found by a consumer writing a shape nobody had thought to test, and round 6's answer (a
|
|
2749
|
+
* thirteen-shape list of "every shape a consumer actually writes") missed the next three anyway,
|
|
2750
|
+
* because a list of observed shapes cannot cover the ones not yet observed.
|
|
2751
|
+
*
|
|
2752
|
+
* So this is a GRID over the four things that actually vary, not a list of anecdotes:
|
|
2753
|
+
*
|
|
2754
|
+
* | axis | values |
|
|
2755
|
+
* |-|-|
|
|
2756
|
+
* | comment style | `//` · `/* *\/` · `/** *\/` · `{/* *\/}` · `{/** *\/}` · `<!-- -->` · JSON member |
|
|
2757
|
+
* | token position | opener line · gutter line · trailing after code |
|
|
2758
|
+
* | where the closer falls | the token's line · its own line · N lines below · n/a |
|
|
2759
|
+
* | what follows | code · blank then code · comment then code · code on the closer's line |
|
|
2760
|
+
*
|
|
2761
|
+
* `configs/oxlint-plugin.test.ts` pins the same grid, under the same row names, for the subset the
|
|
2762
|
+
* plugin can judge (TS/JSX — it never sees CSS, HTML or JSON). The two halves must agree on every
|
|
2763
|
+
* shared cell; add a row to one and it goes in the other in the same commit.
|
|
2764
|
+
*
|
|
2765
|
+
* The unsupported cells are asserted too. "Unsupported" and "silently broken" read identically to a
|
|
2766
|
+
* consumer, and the whole point of the grid is to make them different things.
|
|
2767
|
+
*/
|
|
2768
|
+
/** Wraps JSX children in a component, so a `{/* … *\/}` child is legal source. */
|
|
2769
|
+
const jsxShape = (body: string): string =>
|
|
2770
|
+
`export const C = () => (\n <div>\n${body}\n </div>\n)\n`
|
|
2771
|
+
|
|
2772
|
+
/** Does the file waive its `raw-hex`? — the one question every grid row asks. */
|
|
2773
|
+
const shapeWaives = (relPath: string, src: string): boolean =>
|
|
2774
|
+
checkSource(src, relPath, DEFAULT_GUARD_CONFIG).filter((f) => f.kind === 'raw-hex').length === 0
|
|
2775
|
+
|
|
2776
|
+
describe('theme-allow shape grid', () => {
|
|
2777
|
+
const REASON = 'deliberate legacy value'
|
|
2778
|
+
const A = `theme-allow raw-hex — ${REASON}`
|
|
2779
|
+
const AF = `theme-allow-file raw-hex — ${REASON}`
|
|
2780
|
+
/** The statement the guard reports `raw-hex` on. */
|
|
2781
|
+
const T = `const a = '#f00'`
|
|
2782
|
+
/** The JSX child the guard reports `raw-hex` on. */
|
|
2783
|
+
const JT = `<span style={{ color: '#f00' }} />`
|
|
2784
|
+
const J = jsxShape
|
|
2785
|
+
const waives = shapeWaives
|
|
2786
|
+
|
|
2787
|
+
// ── SUPPORTED — every cell of the grid that waives ────────────────────────
|
|
2788
|
+
it.each([
|
|
2789
|
+
// `//` — no closer to place.
|
|
2790
|
+
['// own line', PATH, `// ${A}\n${T}\n`],
|
|
2791
|
+
['// indented', PATH, `function f() {\n // ${A}\n ${T}\n}\n`],
|
|
2792
|
+
['// no space after the marker', PATH, `//${A}\n${T}\n`],
|
|
2793
|
+
['// trailing', PATH, `${T} // ${A}\n`],
|
|
2794
|
+
['// reason wrapped onto a second // comment', PATH, `// ${A},\n// continued here\n${T}\n`],
|
|
2795
|
+
// `/* */` and `/** */` at statement level, closer in each of its three places.
|
|
2796
|
+
['/* */ own line', PATH, `/* ${A} */\n${T}\n`],
|
|
2797
|
+
['/* */ trailing', PATH, `${T} /* ${A} */\n`],
|
|
2798
|
+
['/** */ own line (docblock opener)', PATH, `/** ${A} */\n${T}\n`],
|
|
2799
|
+
['/* opener + token, closer on its own line', PATH, `/* ${A}\n*/\n${T}\n`],
|
|
2800
|
+
['/* gutter token, closer on the token line', PATH, `/*\n ${A} */\n${T}\n`],
|
|
2801
|
+
['/* gutter token, closer on its own line', PATH, `/*\n ${A}\n*/\n${T}\n`],
|
|
2802
|
+
['/** star gutter, closer on the token line', PATH, `/**\n * ${A} */\n${T}\n`],
|
|
2803
|
+
['/** star gutter, closer on its own line', PATH, `/**\n * ${A}\n */\n${T}\n`],
|
|
2804
|
+
// Prose between the token and the closer — any amount of it.
|
|
2805
|
+
['/** token then 1 prose line', PATH, `/**\n * ${A}\n * more\n */\n${T}\n`],
|
|
2806
|
+
['/** token then 12 prose lines', PATH, `/**\n * ${A}\n${' * p\n'.repeat(12)} */\n${T}\n`],
|
|
2807
|
+
[
|
|
2808
|
+
'/** prose first, token on a later gutter line',
|
|
2809
|
+
PATH,
|
|
2810
|
+
`/**\n * some prose\n * ${A}\n */\n${T}\n`,
|
|
2811
|
+
],
|
|
2812
|
+
// JSX expression containers — the family every disagreement so far has come from.
|
|
2813
|
+
['{/* */} own line', PATH, J(` {/* ${A} */}\n ${JT}`)],
|
|
2814
|
+
['{/* */} trailing on the target line', PATH, J(` ${JT} {/* ${A} */}`)],
|
|
2815
|
+
['{/* opener + token, closer on its own line', PATH, J(` {/* ${A}\n */}\n ${JT}`)],
|
|
2816
|
+
['{/* gutter token, closer on the token line', PATH, J(` {/*\n ${A} */}\n ${JT}`)],
|
|
2817
|
+
[
|
|
2818
|
+
'{/* gutter token, closer on its own line',
|
|
2819
|
+
PATH,
|
|
2820
|
+
J(` {/*\n ${A}\n */}\n ${JT}`),
|
|
2821
|
+
],
|
|
2822
|
+
['{/** star gutter, closer on the token line', PATH, J(` {/**\n * ${A} */}\n ${JT}`)],
|
|
2823
|
+
[
|
|
2824
|
+
'{/** star gutter, closer on its own line',
|
|
2825
|
+
PATH,
|
|
2826
|
+
J(` {/**\n * ${A}\n */}\n ${JT}`),
|
|
2827
|
+
],
|
|
2828
|
+
[
|
|
2829
|
+
'{/** token then 6 prose lines',
|
|
2830
|
+
PATH,
|
|
2831
|
+
J(` {/**\n * ${A}\n${' * p\n'.repeat(6)} */}\n ${JT}`),
|
|
2832
|
+
],
|
|
2833
|
+
['{/* target on the closer line', PATH, J(` {/*\n ${A}\n */}${JT}`)],
|
|
2834
|
+
['{/* closer alone, tab indented', PATH, J(`\t{/*\n\t ${A}\n\t*/}\n\t${JT}`)],
|
|
2835
|
+
[
|
|
2836
|
+
'{/* closer alone, space between */ and }',
|
|
2837
|
+
PATH,
|
|
2838
|
+
J(` {/*\n ${A}\n */ }\n ${JT}`),
|
|
2839
|
+
],
|
|
2840
|
+
[
|
|
2841
|
+
'{/* closer alone, two annotations in one block',
|
|
2842
|
+
PATH,
|
|
2843
|
+
J(` {/*\n ${A}\n theme-allow raw-color-fn — ${REASON}\n */}\n ${JT}`),
|
|
2844
|
+
],
|
|
2845
|
+
// A further comment between the annotation and the code is walked through; a blank line is not.
|
|
2846
|
+
['/** */ then an unrelated // note', PATH, `/** ${A} */\n// an unrelated note\n${T}\n`],
|
|
2847
|
+
[
|
|
2848
|
+
'{/* */} then an unrelated {/* */}',
|
|
2849
|
+
PATH,
|
|
2850
|
+
J(` {/* ${A} */}\n {/* unrelated */}\n ${JT}`),
|
|
2851
|
+
],
|
|
2852
|
+
// The file-declaration form, which is position-independent — pinned in two placements anyway,
|
|
2853
|
+
// because "position-independent" is a claim the parser has to keep making.
|
|
2854
|
+
[
|
|
2855
|
+
'theme-allow-file, {/* closer on its own line',
|
|
2856
|
+
PATH,
|
|
2857
|
+
J(` {/*\n ${AF}\n */}\n ${JT}`),
|
|
2858
|
+
],
|
|
2859
|
+
['theme-allow-file, /** star gutter', PATH, `/**\n * ${AF}\n */\n${T}\n`],
|
|
2860
|
+
// Dialects the plugin never sees — CSS continuation lines, HTML, and the JSON member form.
|
|
2861
|
+
['css trailing', 'src/a.css', `a { color: #f00; /* ${A} */ }\n`],
|
|
2862
|
+
[
|
|
2863
|
+
'css reflowed onto continuation lines',
|
|
2864
|
+
'src/a.css',
|
|
2865
|
+
`a {\n color: var(\n --x,\n #f00\n ); /* ${A} */\n}\n`,
|
|
2866
|
+
],
|
|
2867
|
+
['html comment own line', 'index.html', `<!-- ${A} -->\n<b style="color: #f00"></b>\n`],
|
|
2868
|
+
['html trailing comment', 'index.html', `<b style="color: #f00"></b> <!-- ${A} -->\n`],
|
|
2869
|
+
[
|
|
2870
|
+
'JSON member form',
|
|
2871
|
+
'public/site.webmanifest',
|
|
2872
|
+
`{\n "basalt:theme-allow-file": "raw-hex — ${REASON}",\n "theme_color": "#f00"\n}\n`,
|
|
2873
|
+
],
|
|
2874
|
+
])('%s waives', (_name, relPath, src) => {
|
|
2875
|
+
expect(waives(relPath as string, src as string)).toBe(true)
|
|
2876
|
+
})
|
|
2877
|
+
|
|
2878
|
+
// ── UNSUPPORTED — asserted, so a hole can never pass for a design decision ──
|
|
2879
|
+
it.each([
|
|
2880
|
+
// A blank line is the separation people use for "this comment is not about the next statement".
|
|
2881
|
+
['blank line after a // annotation', PATH, `// ${A}\n\n${T}\n`],
|
|
2882
|
+
['blank line after a {/* */} annotation', PATH, J(` {/* ${A} */}\n\n ${JT}`)],
|
|
2883
|
+
[
|
|
2884
|
+
'blank line after a {/* closer on its own line',
|
|
2885
|
+
PATH,
|
|
2886
|
+
J(` {/*\n ${A}\n */}\n\n ${JT}`),
|
|
2887
|
+
],
|
|
2888
|
+
// Prose that MENTIONS the token is not an annotation — the one false NEGATIVE in the set, and
|
|
2889
|
+
// the reason the token has to START its comment.
|
|
2890
|
+
['mid-sentence in a line comment', PATH, `// we normally write a ${A} here\n${T}\n`],
|
|
2891
|
+
[
|
|
2892
|
+
'mid-sentence in a docblock gutter',
|
|
2893
|
+
PATH,
|
|
2894
|
+
`/**\n * Each value is escaped with a ${A} annotation.\n */\n${T}\n`,
|
|
2895
|
+
],
|
|
2896
|
+
[
|
|
2897
|
+
'mid-sentence in a JSX expression comment',
|
|
2898
|
+
PATH,
|
|
2899
|
+
J(` {/* the shape here is a ${A} comment */}\n ${JT}`),
|
|
2900
|
+
],
|
|
2901
|
+
['inside a string literal', PATH, `const doc = 'theme-allow raw-hex'\n${T}\n`],
|
|
2902
|
+
// An annotation reaches the first line below its comment — not an arbitrary line further down.
|
|
2903
|
+
// A multi-line opening tag therefore needs the annotation beside the attribute, not above the
|
|
2904
|
+
// tag. Both halves agree, and both report rather than silently waiving the wrong node.
|
|
2905
|
+
[
|
|
2906
|
+
'above a multi-line opening tag',
|
|
2907
|
+
PATH,
|
|
2908
|
+
J(` {/* ${A} */}\n <span\n style={{ color: '#f00' }}\n />`),
|
|
2909
|
+
],
|
|
2910
|
+
])('%s does NOT waive', (_name, relPath, src) => {
|
|
2911
|
+
expect(waives(relPath as string, src as string)).toBe(false)
|
|
2912
|
+
})
|
|
2913
|
+
|
|
2914
|
+
// The narrowness of the `*/}` exception: a `}` that is REAL code keeps its trailing
|
|
2915
|
+
// classification, so the annotation does not reach the statement below it.
|
|
2916
|
+
it('a trailing annotation on a real closing brace stays on its own line', () => {
|
|
2917
|
+
const src = `function f() {\n const a = 1\n} // ${A}\nconst b = '#0f0'\n`
|
|
2918
|
+
expect(
|
|
2919
|
+
checkSource(src, PATH, DEFAULT_GUARD_CONFIG)
|
|
2920
|
+
.filter((f) => f.kind === 'raw-hex')
|
|
2921
|
+
.map((f) => f.line),
|
|
2922
|
+
).toEqual([4])
|
|
2923
|
+
})
|
|
2924
|
+
|
|
2925
|
+
// A trailing annotation is scoped to its OWN line — otherwise `const a = '#f00' // theme-allow`
|
|
2926
|
+
// would silently waive the statement below it too.
|
|
2927
|
+
it('a trailing annotation does not reach the line below', () => {
|
|
2928
|
+
const src = `const a = '#f00' // ${A}\nconst b = '#0f0'\n`
|
|
2929
|
+
expect(
|
|
2930
|
+
checkSource(src, PATH, DEFAULT_GUARD_CONFIG).filter((f) => f.kind === 'raw-hex'),
|
|
2931
|
+
).toHaveLength(1)
|
|
2932
|
+
})
|
|
2933
|
+
})
|
|
2934
|
+
|
|
2935
|
+
// ── findAllowAnnotations / neutralizeAllowAnnotation ─────────────────────────────────────────────
|
|
2936
|
+
|
|
2937
|
+
describe('findAllowAnnotations', () => {
|
|
2938
|
+
const cfg = DEFAULT_GUARD_CONFIG
|
|
2939
|
+
|
|
2940
|
+
it('classifies each named id by whose reach it falls in', () => {
|
|
2941
|
+
const src = [
|
|
2942
|
+
`// theme-allow raw-hex — a guard kind`,
|
|
2943
|
+
`const a = '#f00'`,
|
|
2944
|
+
`// theme-allow hand-rolled-plot — a plugin rule, outside checkSource's reach`,
|
|
2945
|
+
`const b = 1`,
|
|
2946
|
+
`// theme-allow raw-hexx — a typo`,
|
|
2947
|
+
`const c = 2`,
|
|
2948
|
+
`// theme-allow`,
|
|
2949
|
+
`const d = 3`,
|
|
2950
|
+
].join('\n')
|
|
2951
|
+
const sites = findAllowAnnotations(src, PATH, cfg)
|
|
2952
|
+
|
|
2953
|
+
expect(sites.map((s) => s.line)).toEqual([1, 3, 5, 7])
|
|
2954
|
+
expect(sites[0]).toMatchObject({ guardKinds: ['raw-hex'], pluginRules: [], bare: false })
|
|
2955
|
+
expect(sites[1]).toMatchObject({ guardKinds: [], pluginRules: ['hand-rolled-plot'] })
|
|
2956
|
+
expect(sites[2]).toMatchObject({ rules: [], unknownRules: ['raw-hexx'], bare: false })
|
|
2957
|
+
expect(sites[3]).toMatchObject({ bare: true, hasReason: false })
|
|
2958
|
+
})
|
|
2959
|
+
|
|
2960
|
+
it('records the file-declaration form once, at the line it is written on', () => {
|
|
2961
|
+
const src = `// theme-allow-file raw-hex — the whole palette fixture\nconst a = '#f00'\n`
|
|
2962
|
+
expect(findAllowAnnotations(src, PATH, cfg)).toMatchObject([
|
|
2963
|
+
{ line: 1, scope: 'file', guardKinds: ['raw-hex'] },
|
|
2964
|
+
])
|
|
2965
|
+
})
|
|
2966
|
+
|
|
2967
|
+
it('carries the source line verbatim, and finds nothing in a file with no annotation', () => {
|
|
2968
|
+
expect(findAllowAnnotations(`const a = '#f00'\n`, PATH, cfg)).toEqual([])
|
|
2969
|
+
expect(
|
|
2970
|
+
findAllowAnnotations(` // theme-allow raw-hex — why not\nconst a = 1\n`, PATH, cfg)[0]?.text,
|
|
2971
|
+
).toBe(' // theme-allow raw-hex — why not')
|
|
2972
|
+
})
|
|
2973
|
+
|
|
2974
|
+
// It shares `collectAllowAnnotations` with `checkSource`, so it cannot list a line the scan does
|
|
2975
|
+
// not honour — the mirrored regex `--audit-allows` carried was already one alternation behind.
|
|
2976
|
+
it('agrees with checkSource about what is prose and what is an annotation', () => {
|
|
2977
|
+
expect(
|
|
2978
|
+
findAllowAnnotations(`// a theme-allow raw-hex goes here\nconst a = 1\n`, PATH, cfg),
|
|
2979
|
+
).toEqual([])
|
|
2980
|
+
expect(
|
|
2981
|
+
findAllowAnnotations(`/** theme-allow raw-hex — deliberate */\nconst a = 1\n`, PATH, cfg),
|
|
2982
|
+
).toHaveLength(1)
|
|
2983
|
+
})
|
|
2984
|
+
|
|
2985
|
+
it('finds the JSON member form in a manifest', () => {
|
|
2986
|
+
const src = `{\n "basalt:theme-allow-file": "raw-hex — brand color"\n}\n`
|
|
2987
|
+
expect(findAllowAnnotations(src, 'public/site.webmanifest', cfg)).toMatchObject([
|
|
2988
|
+
{ line: 2, scope: 'file', guardKinds: ['raw-hex'] },
|
|
2989
|
+
])
|
|
2990
|
+
})
|
|
2991
|
+
})
|
|
2992
|
+
|
|
2993
|
+
describe('neutralizeAllowAnnotation', () => {
|
|
2994
|
+
const cfg = DEFAULT_GUARD_CONFIG
|
|
2995
|
+
|
|
2996
|
+
it('neutralizes ONE annotation and reveals exactly what it suppressed', () => {
|
|
2997
|
+
const src = [
|
|
2998
|
+
`const a = '#f00' // theme-allow raw-hex — first`,
|
|
2999
|
+
`const b = '#0f0' // theme-allow raw-hex — second`,
|
|
3000
|
+
].join('\n')
|
|
3001
|
+
expect(checkSource(src, PATH, cfg).filter((f) => f.kind === 'raw-hex')).toEqual([])
|
|
3002
|
+
|
|
3003
|
+
const probe = neutralizeAllowAnnotation(src, 1, cfg)
|
|
3004
|
+
expect(probe).toContain(NEUTRALIZED_ALLOW_TOKEN)
|
|
3005
|
+
expect(
|
|
3006
|
+
checkSource(probe, PATH, cfg)
|
|
3007
|
+
.filter((f) => f.kind === 'raw-hex')
|
|
3008
|
+
.map((f) => f.line),
|
|
3009
|
+
).toEqual([1])
|
|
3010
|
+
})
|
|
3011
|
+
|
|
3012
|
+
it('leaves a line with no token, and an out-of-range line, untouched', () => {
|
|
3013
|
+
const src = `const a = 1\nconst b = 2\n`
|
|
3014
|
+
expect(neutralizeAllowAnnotation(src, 1, cfg)).toBe(src)
|
|
3015
|
+
expect(neutralizeAllowAnnotation(src, 99, cfg)).toBe(src)
|
|
3016
|
+
})
|
|
3017
|
+
|
|
3018
|
+
it('the neutralized token shares no substring with the annotation it replaces', () => {
|
|
3019
|
+
expect(NEUTRALIZED_ALLOW_TOKEN).not.toContain(cfg.allowComment)
|
|
3020
|
+
expect(NEUTRALIZED_ALLOW_TOKEN.includes('theme-allow')).toBe(false)
|
|
3021
|
+
})
|
|
3022
|
+
})
|
|
3023
|
+
|
|
3024
|
+
describe('PLUGIN_RULE_IDS', () => {
|
|
3025
|
+
it('names no guard kind — the two registries are disjoint by construction', () => {
|
|
3026
|
+
for (const id of PLUGIN_RULE_IDS) expect(Object.hasOwn(GUARD_RULES, id)).toBe(false)
|
|
3027
|
+
})
|
|
3028
|
+
})
|
package/src/guard/index.ts
CHANGED
|
@@ -400,7 +400,7 @@ const RAW_CHART_LEGEND_ARRAY = /<ChartLegend\b[^>]*?\bitems\s*=\s*\{\s*\[/g
|
|
|
400
400
|
// alternative so their `>` never terminates the tag early. A bare `>` comparison inside a prop
|
|
401
401
|
// expression still ends the match (accepted limitation of the bounded scan).
|
|
402
402
|
const CHART_ENTRY_POINT_TAG =
|
|
403
|
-
/<(?:MultiLine|Bars|Donut|DualPanel|Heatmap|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g
|
|
403
|
+
/<(?:MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g
|
|
404
404
|
const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/
|
|
405
405
|
|
|
406
406
|
// Raw lowercase form-control element — line-scoped, same shape as RAW_VISX_AXIS. `\b` after the
|
|
@@ -661,8 +661,12 @@ export function unmatchedExemptPatterns(
|
|
|
661
661
|
* but they ARE valid ids in a `theme-allow`, so an annotation scoped to one of them must parse as
|
|
662
662
|
* accountable rather than be reported as unscoped. Kept as a literal list because the plugin is a
|
|
663
663
|
* standalone `.js` file this module must not import (see the plugin's own header).
|
|
664
|
+
*
|
|
665
|
+
* Exported for `check-theme --audit-allows`: an annotation naming one of these is outside the
|
|
666
|
+
* guard's reach, and judging it needs a `oxlint` run rather than a `checkSource` one. Keep in step
|
|
667
|
+
* with the plugin's own `KNOWN_RULE_IDS`; `configs/oxlint-plugin.test.ts` asserts the two agree.
|
|
664
668
|
*/
|
|
665
|
-
const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
|
|
669
|
+
export const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
|
|
666
670
|
'no-raw-font-size',
|
|
667
671
|
'raw-size-literal',
|
|
668
672
|
'card-inset',
|
|
@@ -767,8 +771,45 @@ function parseAllowAnnotation(rest: string, scope: AllowScope): AllowAnnotation
|
|
|
767
771
|
/** How far back a trailing CSS annotation reaches for the declaration it terminates. */
|
|
768
772
|
const MAX_CSS_CONTINUATION_LINES = 8
|
|
769
773
|
|
|
770
|
-
/**
|
|
771
|
-
|
|
774
|
+
/**
|
|
775
|
+
* The 0-based line the annotation's OWN comment closes on.
|
|
776
|
+
*
|
|
777
|
+
* A comment-only annotation waives the first line below its comment that is not itself comment, so
|
|
778
|
+
* the walk needs to know how far the annotation's own comment reaches. It used to substitute a
|
|
779
|
+
* budget — walk forward at most eight lines — which was wrong in two ways nothing could see:
|
|
780
|
+
*
|
|
781
|
+
* 1. **A comment block longer than the budget.** A docblock with ~12 lines of reason between the
|
|
782
|
+
* token and its `*\/` ran the walk out before it reached the code, while the oxlint plugin
|
|
783
|
+
* (which reads the comment NODE, so length is free) waived it. Nothing about the shape said so;
|
|
784
|
+
* the annotation just stopped working past a line count nobody could count.
|
|
785
|
+
* 2. **A blank line inside the block.** Outside a comment a blank line SEPARATES — it is how people
|
|
786
|
+
* say "this comment is not about the next statement" — but a blank gutter line in the middle of
|
|
787
|
+
* a docblock is just prose, and ending the walk there dropped the waiver.
|
|
788
|
+
*
|
|
789
|
+
* Knowing where the comment ends settles both: inside it, keep going; past it, the ordinary
|
|
790
|
+
* comment-run rules apply (see the walk in {@link collectAllowAnnotations}, and
|
|
791
|
+
* {@link isCommentOnly} for the third hole this pass closed).
|
|
792
|
+
*
|
|
793
|
+
* An unterminated block comment reports its own line — the conservative answer for a file that does
|
|
794
|
+
* not parse anyway, and it keeps the walk from running to EOF.
|
|
795
|
+
*/
|
|
796
|
+
function commentBlockEnd(
|
|
797
|
+
lines: readonly string[],
|
|
798
|
+
index: number,
|
|
799
|
+
tokenAt: number,
|
|
800
|
+
syntax: GuardSyntax,
|
|
801
|
+
): number {
|
|
802
|
+
const line = lines[index] ?? ''
|
|
803
|
+
// A line comment closes at its own end of line. CSS has no `//`, so the test is TS-only: there,
|
|
804
|
+
// a `//`-looking sequence is text and the conservative same-line answer is the right one anyway.
|
|
805
|
+
if (syntax === 'ts' && /\/\/\s*$/.test(line.slice(0, tokenAt))) return index
|
|
806
|
+
const closer = syntax === 'markup' ? '-->' : '*/'
|
|
807
|
+
if (line.slice(tokenAt).includes(closer)) return index
|
|
808
|
+
for (let ahead = index + 1; ahead < lines.length; ahead++) {
|
|
809
|
+
if ((lines[ahead] ?? '').includes(closer)) return ahead
|
|
810
|
+
}
|
|
811
|
+
return index
|
|
812
|
+
}
|
|
772
813
|
|
|
773
814
|
/**
|
|
774
815
|
* A comment opener sitting immediately before the annotation token.
|
|
@@ -784,6 +825,39 @@ const COMMENT_OPENER_BEFORE = /(?:\/\/|\/\*|<!--)\s*$/
|
|
|
784
825
|
/** A line that is NOTHING but a comment — the placement that waives the line below it. */
|
|
785
826
|
const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/
|
|
786
827
|
|
|
828
|
+
/**
|
|
829
|
+
* A JSX expression container closing immediately after the comment that filled it — the `*\/}` of a
|
|
830
|
+
* `{/* … *\/}` whose annotation wrapped onto its own line.
|
|
831
|
+
*
|
|
832
|
+
* `stripComments` blanks the comment and leaves the bare `}` behind, so the line read as CODE and
|
|
833
|
+
* the annotation was classified TRAILING — scoped to a line that holds nothing but a brace, and
|
|
834
|
+
* therefore waiving nothing. That is the shape linewatch writes for every hand-composed chart axis
|
|
835
|
+
* (`{/*` on one line, the annotation and `*\/}` on the next), which worked only because the rules it
|
|
836
|
+
* names live in the oxlint plugin, whose own placement test is comment-node-based and never saw the
|
|
837
|
+
* brace. Any guard kind annotated this way was silently unwaivable.
|
|
838
|
+
*
|
|
839
|
+
* Deliberately narrow: the `}` must abut the comment CLOSE. A `}` that is real code (`} // theme-
|
|
840
|
+
* allow …` closing a block) keeps its trailing classification, so the annotation stays on its own
|
|
841
|
+
* line rather than reaching the statement below.
|
|
842
|
+
*/
|
|
843
|
+
const JSX_COMMENT_CLOSE = /\*\/\s*\}\s*$/
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Is this line NOTHING but comment? — one definition, used both to classify the annotation's own
|
|
847
|
+
* line (own-line vs trailing) and to walk the lines under it.
|
|
848
|
+
*
|
|
849
|
+
* The two used to be written out separately, and the walk's copy was the one missing the
|
|
850
|
+
* {@link JSX_COMMENT_CLOSE} clause: a `*\/}` alone on its line read as the code `}`, so the walk
|
|
851
|
+
* stopped ON the closer and the annotation waived a brace. Two consumers isolated that
|
|
852
|
+
* independently — `{/* … *\/}` and `{/** … *\/}` — each with a probe pair differing by one newline,
|
|
853
|
+
* while the oxlint plugin (whose test is comment-node-based and never sees the brace) waived both.
|
|
854
|
+
* A shared predicate is what stops the two placements from drifting apart again.
|
|
855
|
+
*/
|
|
856
|
+
function isCommentOnly(raw: string, stripped: string): boolean {
|
|
857
|
+
const code = stripped.trim()
|
|
858
|
+
return code === '' || COMMENT_ONLY_LINE.test(raw) || (code === '}' && JSX_COMMENT_CLOSE.test(raw))
|
|
859
|
+
}
|
|
860
|
+
|
|
787
861
|
/**
|
|
788
862
|
* Everything allowed between the comment opener (or the start of the line) and the annotation
|
|
789
863
|
* token, for the token to count as an ANNOTATION rather than prose that mentions one.
|
|
@@ -795,9 +869,14 @@ const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/
|
|
|
795
869
|
* own waivers in a docblock and thereby disarmed the file — the third hole found in this contract
|
|
796
870
|
* in two rounds, and the only one that was a false NEGATIVE.
|
|
797
871
|
*
|
|
798
|
-
* So an annotation must START its comment, or start a line inside one: after `//`,
|
|
799
|
-
* block-comment gutter `*`, or nothing but leading whitespace.
|
|
800
|
-
* writes qualifies; a sentence about the escape hatch does not.
|
|
872
|
+
* So an annotation must START its comment, or start a line inside one: after `//`, `/*` (or a
|
|
873
|
+
* docblock's `/**`), `<!--`, a block-comment gutter `*`, or nothing but leading whitespace.
|
|
874
|
+
* Everything a consumer actually writes qualifies; a sentence about the escape hatch does not.
|
|
875
|
+
*
|
|
876
|
+
* `/**` is in the alternation because the oxlint plugin's copy of this test always accepted it and
|
|
877
|
+
* this one did not: a one-line `/** theme-allow raw-hex — why *\/` waived under `oxlint` and
|
|
878
|
+
* reported under `check-theme`. The two halves of one contract must not disagree about what an
|
|
879
|
+
* annotation IS — only about which rules each can judge.
|
|
801
880
|
*
|
|
802
881
|
* What the annotation then WAIVES is a separate, fail-closed question — see
|
|
803
882
|
* {@link parseAllowAnnotation}. A word in the id slot that names no rule waives nothing, so
|
|
@@ -809,7 +888,7 @@ const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/
|
|
|
809
888
|
* it anyway: the kind that fires is the UNSUPPRESSED one (`raw-hex`), not `theme-allow-unscoped`,
|
|
810
889
|
* and downgrading `raw-hex` to waive-by-default is the hole this closed.
|
|
811
890
|
*/
|
|
812
|
-
const ANNOTATION_PREFIX = /(
|
|
891
|
+
const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*\*?|<!--|^\s*\*)\s*$/
|
|
813
892
|
|
|
814
893
|
/** Escape a config-supplied string for literal use inside a RegExp. */
|
|
815
894
|
function escapeRegExp(value: string): string {
|
|
@@ -907,7 +986,12 @@ function collectAllowAnnotations(
|
|
|
907
986
|
}
|
|
908
987
|
|
|
909
988
|
/** Place one parsed annotation: file declarations reach everywhere, line ones the 3 placements. */
|
|
910
|
-
const record = (
|
|
989
|
+
const record = (
|
|
990
|
+
index: number,
|
|
991
|
+
annotation: AllowAnnotation,
|
|
992
|
+
trailing: boolean,
|
|
993
|
+
tokenAt: number,
|
|
994
|
+
): void => {
|
|
911
995
|
declared.push({ line: index + 1, annotation })
|
|
912
996
|
if (annotation.scope === 'file') {
|
|
913
997
|
fileScoped.push(annotation)
|
|
@@ -915,16 +999,20 @@ function collectAllowAnnotations(
|
|
|
915
999
|
}
|
|
916
1000
|
add(index, annotation)
|
|
917
1001
|
if (!trailing) {
|
|
918
|
-
// The first line below
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
// the separation people use to mean "this comment is not about the next
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1002
|
+
// The first CODE line below the comment. A docblock's `*/` (or the rest of a wrapped reason)
|
|
1003
|
+
// sits between the annotation and the code it is about, so `index + 1` alone meant the
|
|
1004
|
+
// natural multi-line shape silently waived nothing — argo hit it three times in one upgrade,
|
|
1005
|
+
// each looking like a correct annotation. A BLANK line outside the comment still ends the
|
|
1006
|
+
// block: that is the separation people use to mean "this comment is not about the next
|
|
1007
|
+
// statement". Inside it, a blank gutter line is just prose.
|
|
1008
|
+
const closesAt = commentBlockEnd(lines, index, tokenAt, syntax)
|
|
1009
|
+
for (let ahead = 1; index + ahead < lines.length; ahead++) {
|
|
1010
|
+
const at = index + ahead
|
|
1011
|
+
add(at, annotation)
|
|
1012
|
+
if (at <= closesAt) continue
|
|
1013
|
+
const raw = lines[at] ?? ''
|
|
927
1014
|
if (raw.trim() === '') break
|
|
1015
|
+
if (!isCommentOnly(raw, codeLines[at] ?? '')) break
|
|
928
1016
|
}
|
|
929
1017
|
return
|
|
930
1018
|
}
|
|
@@ -942,12 +1030,17 @@ function collectAllowAnnotations(
|
|
|
942
1030
|
for (let i = 0; i < lines.length; i++) {
|
|
943
1031
|
const line = lines[i] ?? ''
|
|
944
1032
|
const stripped = codeLines[i] ?? ''
|
|
945
|
-
const isCommentOnlyLine =
|
|
1033
|
+
const isCommentOnlyLine = isCommentOnly(line, stripped)
|
|
946
1034
|
|
|
947
1035
|
// The JSON member form. Markup-only: in TS/CSS that string would be code, not an annotation.
|
|
948
1036
|
if (syntax === 'markup') {
|
|
949
1037
|
for (const m of line.matchAll(jsonKey)) {
|
|
950
|
-
record(
|
|
1038
|
+
record(
|
|
1039
|
+
i,
|
|
1040
|
+
parseAllowAnnotation(m[2] ?? '', m[1] === undefined ? 'line' : 'file'),
|
|
1041
|
+
false,
|
|
1042
|
+
m.index,
|
|
1043
|
+
)
|
|
951
1044
|
}
|
|
952
1045
|
}
|
|
953
1046
|
|
|
@@ -965,6 +1058,7 @@ function collectAllowAnnotations(
|
|
|
965
1058
|
i,
|
|
966
1059
|
parseAllowAnnotation(rest, m[1] === undefined ? 'line' : 'file'),
|
|
967
1060
|
!isCommentOnlyLine,
|
|
1061
|
+
at,
|
|
968
1062
|
)
|
|
969
1063
|
}
|
|
970
1064
|
}
|
|
@@ -2050,3 +2144,98 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2050
2144
|
severity: severityOf(f.kind, cfg),
|
|
2051
2145
|
}))
|
|
2052
2146
|
}
|
|
2147
|
+
|
|
2148
|
+
// ── Allow-annotation enumeration (check-theme --audit-allows) ─────────────────────────────────────
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* One `theme-allow` annotation as WRITTEN, classified by whose reach its ids fall in.
|
|
2152
|
+
*
|
|
2153
|
+
* `guardKinds` is what `checkSource` can judge by re-running itself with the annotation
|
|
2154
|
+
* neutralized. `pluginRules` is what only an `oxlint` run can judge. `unknownRules` names no rule
|
|
2155
|
+
* at all. All three empty with `bare: true` is the legacy blanket form.
|
|
2156
|
+
*/
|
|
2157
|
+
export type AllowAnnotationSite = {
|
|
2158
|
+
/** 1-based line the annotation is written on. */
|
|
2159
|
+
readonly line: number
|
|
2160
|
+
/** `'file'` for the `theme-allow-file` declaration form, `'line'` otherwise. */
|
|
2161
|
+
readonly scope: 'line' | 'file'
|
|
2162
|
+
/** Every id the annotation names that resolves to a real rule, `basalt/` prefix stripped. */
|
|
2163
|
+
readonly rules: readonly string[]
|
|
2164
|
+
/** The subset of `rules` `checkSource` owns. */
|
|
2165
|
+
readonly guardKinds: readonly GuardKind[]
|
|
2166
|
+
/** The subset of `rules` the oxlint plugin owns — outside `checkSource`'s reach. */
|
|
2167
|
+
readonly pluginRules: readonly string[]
|
|
2168
|
+
/** Words that occupied the id slot but name no rule. Their presence makes the parse fail closed. */
|
|
2169
|
+
readonly unknownRules: readonly string[]
|
|
2170
|
+
/** A written reason follows the ids. */
|
|
2171
|
+
readonly hasReason: boolean
|
|
2172
|
+
/** The legacy blanket form — no ids at all, known or unknown. Only covers anything at line scope. */
|
|
2173
|
+
readonly bare: boolean
|
|
2174
|
+
/** The source line, verbatim. */
|
|
2175
|
+
readonly text: string
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Every allow annotation in one file, with the ids it names classified by reach.
|
|
2180
|
+
*
|
|
2181
|
+
* Pure, and it shares `collectAllowAnnotations` with {@link checkSource} — so a caller enumerating
|
|
2182
|
+
* annotations sees exactly the set the scan honours, rather than a second regex that can drift from
|
|
2183
|
+
* it. `--audit-allows` carried such a mirror and it was already one alternation behind.
|
|
2184
|
+
*
|
|
2185
|
+
* A file-scoped declaration appears once, at the line it is written on.
|
|
2186
|
+
*
|
|
2187
|
+
* @example
|
|
2188
|
+
* for (const site of findAllowAnnotations(text, rel, cfg)) {
|
|
2189
|
+
* if (site.pluginRules.length > 0) probeWithOxlint(neutralizeAllowAnnotation(text, site.line, cfg))
|
|
2190
|
+
* }
|
|
2191
|
+
*/
|
|
2192
|
+
export function findAllowAnnotations(
|
|
2193
|
+
text: string,
|
|
2194
|
+
relPath: string,
|
|
2195
|
+
cfg: GuardConfig,
|
|
2196
|
+
): AllowAnnotationSite[] {
|
|
2197
|
+
const lines = text.split('\n')
|
|
2198
|
+
const syntax = guardSyntaxFor(relPath)
|
|
2199
|
+
const codeText = syntax === 'markup' ? stripMarkupComments(text) : stripComments(text, syntax)
|
|
2200
|
+
const { declared } = collectAllowAnnotations(
|
|
2201
|
+
lines,
|
|
2202
|
+
codeText.split('\n'),
|
|
2203
|
+
cfg.allowComment,
|
|
2204
|
+
syntax,
|
|
2205
|
+
)
|
|
2206
|
+
return declared.map(({ line, annotation }) => ({
|
|
2207
|
+
line,
|
|
2208
|
+
scope: annotation.scope,
|
|
2209
|
+
rules: [...annotation.rules],
|
|
2210
|
+
guardKinds: annotation.rules.filter((id): id is GuardKind => Object.hasOwn(GUARD_RULES, id)),
|
|
2211
|
+
pluginRules: annotation.rules.filter(
|
|
2212
|
+
(id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id),
|
|
2213
|
+
),
|
|
2214
|
+
unknownRules: [...annotation.unknownRules],
|
|
2215
|
+
hasReason: annotation.hasReason,
|
|
2216
|
+
bare: annotation.rules.length === 0 && annotation.unknownRules.length === 0,
|
|
2217
|
+
text: lines[line - 1] ?? '',
|
|
2218
|
+
}))
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/** The token a neutralized annotation is rewritten to. Shares no substring with `theme-allow`. */
|
|
2222
|
+
export const NEUTRALIZED_ALLOW_TOKEN = 'basalt-audit-neutralized'
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* `text` with the allow token on ONE (1-based) line rewritten to {@link NEUTRALIZED_ALLOW_TOKEN},
|
|
2226
|
+
* leaving every other annotation intact.
|
|
2227
|
+
*
|
|
2228
|
+
* The probe half of an audit: re-scan the result and the findings that appear are exactly what that
|
|
2229
|
+
* one annotation suppresses. Shipped rather than left to each caller so the guard probe and the
|
|
2230
|
+
* oxlint probe neutralize identically — a `--audit-allows` verdict that differs between the two
|
|
2231
|
+
* halves by a substitution detail is worse than no verdict.
|
|
2232
|
+
*
|
|
2233
|
+
* A line with no token comes back unchanged.
|
|
2234
|
+
*/
|
|
2235
|
+
export function neutralizeAllowAnnotation(text: string, line: number, cfg: GuardConfig): string {
|
|
2236
|
+
const lines = text.split('\n')
|
|
2237
|
+
const target = lines[line - 1]
|
|
2238
|
+
if (target === undefined) return text
|
|
2239
|
+
lines[line - 1] = target.replaceAll(cfg.allowComment, NEUTRALIZED_ALLOW_TOKEN)
|
|
2240
|
+
return lines.join('\n')
|
|
2241
|
+
}
|