@@ -16,7 +16,12 @@ import {
1616import { encodeSigned , encodeUnsigned } from "../vlq.ts" ;
1717import { decode , DecodeMode } from "./decode.ts" ;
1818import type { IndexSourceMapJson , SourceMapJson } from "../scopes.ts" ;
19- import { GeneratedRangeFlags , OriginalScopeFlags , Tag } from "../codec.ts" ;
19+ import {
20+ EncodedTag ,
21+ GeneratedRangeFlags ,
22+ OriginalScopeFlags ,
23+ Tag ,
24+ } from "../codec.ts" ;
2025
2126class ItemEncoder {
2227 #encodedItems: string [ ] = [ ] ;
@@ -730,6 +735,7 @@ describe("decode", () => {
730735 encoder . addUnsignedVLQs ( Tag . VENDOR_EXTENSION , 0 ) ;
731736 encoder . finishItem ( ) ;
732737 const ext = encoder . encode ( ) ;
738+ assertEquals ( ext , "VA" ) ;
733739 const map = createMap ( {
734740 scopes : [ ext ] ,
735741 ranges : ext ,
@@ -743,6 +749,186 @@ describe("decode", () => {
743749 } ) ;
744750 } ) ;
745751
752+ it ( "ignores vendor extensions with trailing VLQs" , ( ) => {
753+ const map = createMap ( {
754+ scopes : [ "VAEFG" ] ,
755+ ranges : "VACD" ,
756+ names : [ "x_ext_item" ] ,
757+ } ) ;
758+
759+ assertEquals ( decode ( map , { mode : DecodeMode . STRICT } ) , {
760+ scopes : [ [ ] ] ,
761+ ranges : [ ] ,
762+ hasVariableAndBindingInfo : false ,
763+ } ) ;
764+ } ) ;
765+
766+ describe ( "vendor extensions interspersed with known items" , ( ) => {
767+ /**
768+ * Inserts a vendor extension item before, between and after every item of
769+ * `encoded`. Every other vendor extension item carries trailing VLQs.
770+ */
771+ function intersperseVendorExtensions (
772+ encoded : string ,
773+ nameIdx : number ,
774+ ) : string {
775+ const plain = EncodedTag . VENDOR_EXTENSION + encodeUnsigned ( nameIdx ) ;
776+ const withTrailing = plain + encodeUnsigned ( 42 ) + encodeSigned ( - 7 ) +
777+ encodeUnsigned ( 1000 ) ;
778+ const items : string [ ] = [ plain ] ;
779+ encoded . split ( "," ) . forEach ( ( item , i ) => {
780+ items . push ( item , i % 2 === 0 ? withTrailing : plain ) ;
781+ } ) ;
782+ return items . join ( "," ) ;
783+ }
784+
785+ function buildInfo ( ) {
786+ return new ScopeInfoBuilder ( )
787+ . startSource ( )
788+ . startScope ( 0 , 0 , {
789+ key : "fn" ,
790+ name : "foo" ,
791+ kind : "function" ,
792+ isStackFrame : true ,
793+ variables : [ "a" , "b" ] ,
794+ } )
795+ . startScope ( 2 , 4 , { key : "block" , kind : "block" , variables : [ "c" ] } )
796+ . endScope ( 2 , 10 )
797+ . startScope ( 2 , 12 , { kind : "block" } )
798+ . endScope ( 5 , 0 )
799+ . endScope ( 20 , 0 )
800+ . startScope ( 21 , 0 , { name : "bar" } )
801+ . endScope ( 30 , 0 )
802+ . endSource ( )
803+ . addNullSource ( )
804+ . startSource ( )
805+ . startScope ( 0 , 0 , { key : "other" , variables : [ "d" ] } )
806+ . endScope ( 5 , 0 )
807+ . endSource ( )
808+ . startRange ( 0 , 0 , {
809+ scopeKey : "fn" ,
810+ isStackFrame : true ,
811+ values : [ "x" , null ] ,
812+ } )
813+ . startRange ( 0 , 5 , {
814+ scopeKey : "block" ,
815+ values : [ [
816+ {
817+ value : "y" ,
818+ from : { line : 0 , column : 5 } ,
819+ to : { line : 0 , column : 8 } ,
820+ } ,
821+ {
822+ value : "z" ,
823+ from : { line : 0 , column : 8 } ,
824+ to : { line : 0 , column : 15 } ,
825+ } ,
826+ ] ] ,
827+ } )
828+ . endRange ( 0 , 15 )
829+ . startRange ( 1 , 0 , {
830+ scopeKey : "other" ,
831+ isHidden : true ,
832+ values : [ "w" ] ,
833+ callSite : { sourceIndex : 0 , line : 3 , column : 2 } ,
834+ } )
835+ . endRange ( 1 , 20 )
836+ . endRange ( 10 , 0 )
837+ . build ( ) ;
838+ }
839+
840+ it ( "ignores them between scope items in strict mode" , ( ) => {
841+ const map = encode ( buildInfo ( ) ) ;
842+ const expected = decode ( map , { mode : DecodeMode . STRICT } ) ;
843+ assertExists ( map . scopes ) ;
844+ assertEquals ( map . scopes . length , 3 ) ;
845+
846+ map . names ! . push ( "x_vendor" ) ;
847+ const nameIdx = map . names ! . length - 1 ;
848+ map . scopes = map . scopes . map ( ( s ) =>
849+ s === null ? null : intersperseVendorExtensions ( s , nameIdx )
850+ ) ;
851+
852+ assertEquals ( decode ( map , { mode : DecodeMode . STRICT } ) , expected ) ;
853+ assert ( expected . hasVariableAndBindingInfo ) ;
854+ } ) ;
855+
856+ it ( "ignores them between range items in strict mode" , ( ) => {
857+ const map = encode ( buildInfo ( ) ) ;
858+ const expected = decode ( map , { mode : DecodeMode . STRICT } ) ;
859+ assertExists ( map . ranges ) ;
860+
861+ map . names ! . push ( "x_vendor" ) ;
862+ const nameIdx = map . names ! . length - 1 ;
863+ map . ranges = map . ranges . map ( ( r ) =>
864+ intersperseVendorExtensions ( r , nameIdx )
865+ ) ;
866+
867+ assertEquals ( decode ( map , { mode : DecodeMode . STRICT } ) , expected ) ;
868+ assert ( expected . hasVariableAndBindingInfo ) ;
869+ } ) ;
870+
871+ it ( "handles multi-character name indices" , ( ) => {
872+ const map = encode ( buildInfo ( ) ) ;
873+ const expected = decode ( map , { mode : DecodeMode . STRICT } ) ;
874+ assertExists ( map . scopes ?. [ 0 ] ) ;
875+ assertExists ( map . ranges ?. [ 0 ] ) ;
876+
877+ // Pad "names" so the vendor extension name requires a multi-character VLQ.
878+ const nameIdx = 1000 ;
879+ while ( map . names ! . length < nameIdx ) map . names ! . push ( "pad" ) ;
880+ map . names ! . push ( "x_vendor" ) ;
881+ assert ( encodeUnsigned ( nameIdx ) . length > 1 ) ;
882+
883+ map . scopes [ 0 ] = intersperseVendorExtensions ( map . scopes [ 0 ] , nameIdx ) ;
884+ map . ranges [ 0 ] = intersperseVendorExtensions ( map . ranges [ 0 ] , nameIdx ) ;
885+
886+ assertEquals ( decode ( map , { mode : DecodeMode . STRICT } ) , expected ) ;
887+ } ) ;
888+
889+ it ( "ignores out-of-bounds name indices in strict mode" , ( ) => {
890+ const map = encode ( buildInfo ( ) ) ;
891+ const expected = decode ( map , { mode : DecodeMode . STRICT } ) ;
892+ assertExists ( map . scopes ?. [ 0 ] ) ;
893+ assertExists ( map . ranges ?. [ 0 ] ) ;
894+
895+ const nameIdx = map . names ! . length + 500 ;
896+ map . scopes [ 0 ] = intersperseVendorExtensions ( map . scopes [ 0 ] , nameIdx ) ;
897+ map . ranges [ 0 ] = intersperseVendorExtensions ( map . ranges [ 0 ] , nameIdx ) ;
898+
899+ assertEquals ( decode ( map , { mode : DecodeMode . STRICT } ) , expected ) ;
900+ } ) ;
901+ } ) ;
902+
903+ describe ( "legacy vendor extension tags" , ( ) => {
904+ // The spec used to reserve "/" for vendor extensions (tc39/ecma426#278
905+ // changed it to "V"). "/" has the continuation bit set, so "/A" decodes
906+ // to tag 31. Older versions of this codec emitted 0x63 ("jD") instead.
907+ const legacyItems : [ string , number ] [ ] = [ [ "/A" , 31 ] , [ "jDA" , 0x63 ] ] ;
908+
909+ for ( const [ item , tag ] of legacyItems ) {
910+ it ( `throws for '${ item } ' in scopes in strict mode` , ( ) => {
911+ const map = createMap ( { scopes : [ item ] , names : [ "x_vendor" ] } ) ;
912+
913+ assertThrows (
914+ ( ) => decode ( map , { mode : DecodeMode . STRICT } ) ,
915+ Error ,
916+ `Encountered illegal item tag ${ tag } ` ,
917+ ) ;
918+ } ) ;
919+
920+ it ( `throws for '${ item } ' in ranges in strict mode` , ( ) => {
921+ const map = createMap ( { ranges : item , names : [ "x_vendor" ] } ) ;
922+
923+ assertThrows (
924+ ( ) => decode ( map , { mode : DecodeMode . STRICT } ) ,
925+ Error ,
926+ `Encountered illegal item tag ${ tag } ` ,
927+ ) ;
928+ } ) ;
929+ }
930+ } ) ;
931+
746932 it ( "throws for invalid item tags in strict mode" , ( ) => {
747933 const encoder = new ItemEncoder ( ) ;
748934 encoder . addUnsignedVLQs ( 42 , 1 , 2 , 3 ) ;
0 commit comments